Как использовать API перетаскивания HTML в React

Опубликовано: 2022-03-10
Краткое резюме ↬ В этом руководстве мы создадим компонент перетаскивания React для загрузки файлов и изображений. В процессе мы узнаем об API перетаскивания HTML. Мы также узнаем, как использовать хук useReducer для управления состоянием в функциональном компоненте React.

API перетаскивания — одна из самых крутых функций HTML. Это помогает нам реализовать функции перетаскивания в веб-браузерах.

В текущем контексте мы будем перетаскивать файлы из-за пределов браузера. При удалении файлов мы помещаем их в список и отображаем их имена. Имея файлы на руках, мы могли бы затем выполнить какую-либо другую операцию с файлами, например, загрузить их на облачный сервер.

В этом руководстве мы сосредоточимся на том, как реализовать действие перетаскивания в приложении React. Если вам нужна простая реализация JavaScript , возможно, вы сначала хотели бы прочитать «Как сделать загрузчик файлов с помощью перетаскивания с помощью ванильного JavaScript», отличный учебник, написанный Джозефом Циммерманом не так давно.

События dragenter , dragleave , dragover и drop

Существует восемь различных событий перетаскивания. Каждый из них срабатывает на разных этапах операции перетаскивания. В этом уроке мы сосредоточимся на четырех, которые запускаются, когда элемент помещается в зону перетаскивания: dragenter , dragleave , dragover и drop .

  1. Событие dragenter срабатывает, когда перетаскиваемый элемент попадает в допустимую цель перетаскивания.
  2. Событие dragleave срабатывает, когда перетаскиваемый элемент покидает допустимую цель перетаскивания.
  3. Событие dragover срабатывает, когда перетаскиваемый элемент перетаскивается на допустимую цель перетаскивания. (Он срабатывает каждые несколько сотен миллисекунд.)
  4. Событие drop срабатывает, когда элемент падает на допустимую цель перетаскивания, т. е. перетаскивается и отпускается.

Мы можем превратить любой HTML-элемент в допустимую цель перетаскивания, определив атрибуты обработчиков событий ondragover и ondrop .

Вы можете узнать все о восьми событиях из веб-документов MDN.

Еще после прыжка! Продолжить чтение ниже ↓

События перетаскивания в React

Для начала клонируйте репозиторий учебника по этому URL-адресу:

 https://github.com/chidimo/react-dnd.git

Проверьте ветку 01-start . Убедитесь, что у вас также установлена yarn . Вы можете получить его на сайте yarnpkg.com.

Но если хотите, создайте новый проект React и замените содержимое App.js кодом ниже:

 import React from 'react'; import './App.css'; function App() { return ( <div className="App"> <h1>React drag-and-drop component</h1> </div> ); } export default App;

Кроме того, замените содержимое App.css следующим стилем CSS:

 .App { margin: 2rem; text-align: center; } h1 { color: #07F; } .drag-drop-zone { padding: 2rem; text-align: center; background: #07F; border-radius: 0.5rem; box-shadow: 5px 5px 10px #C0C0C0; } .drag-drop-zone p { color: #FFF; } .drag-drop-zone.inside-drag-area { opacity: 0.7; } .dropped-files li { color: #07F; padding: 3px; text-align: left; font-weight: bold; }

Если вы клонировали репозиторий, введите следующие команды (по порядку), чтобы запустить приложение:

 yarn # install dependencies yarn start # start the app

Следующим шагом является создание компонента перетаскивания. Создайте файл DragAndDrop.js внутри папки src/ . Введите следующую функцию внутри файла:

 import React from 'react'; const DragAndDrop = props => { const handleDragEnter = e => { e.preventDefault(); e.stopPropagation(); }; const handleDragLeave = e => { e.preventDefault(); e.stopPropagation(); }; const handleDragOver = e => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = e => { e.preventDefault(); e.stopPropagation(); }; return ( <div className={'drag-drop-zone'} onDrop={e => handleDrop(e)} onDragOver={e => handleDragOver(e)} onDragEnter={e => handleDragEnter(e)} onDragLeave={e => handleDragLeave(e)} > <p>Drag files here to upload</p> </div> ); }; export default DragAndDrop;

В возвращаемом div мы определили наши атрибуты обработчика HTML -событий фокуса. Вы можете видеть, что единственное отличие от чистого HTML — это Camel-Case.

Теперь div является допустимой целью перетаскивания, так как мы определили атрибуты обработчиков событий onDragOver и onDrop .

Мы также определили функции для обработки этих событий. Каждая из этих функций-обработчиков получает объект события в качестве аргумента.

Для каждого из обработчиков событий мы вызываем preventDefault() , чтобы запретить браузеру выполнять поведение по умолчанию. Браузер по умолчанию открывает перетащенный файл. Мы также вызываем stopPropagation() , чтобы убедиться, что событие не распространяется от дочерних элементов к родительским.

Импортируйте компонент DragAndDrop в компонент App и отобразите его под заголовком.

 <div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop /> </div>

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

Зона сброса
div для преобразования в зону перетаскивания (большой предварительный просмотр)

Если вы следуете репо, соответствующая ветка — 02-start-dragndrop

Управление состоянием с помощью useReducer

Нашим следующим шагом будет написание логики для каждого из наших обработчиков событий. Прежде чем мы это сделаем, мы должны подумать, как мы собираемся отслеживать удаленные файлы. Здесь мы начинаем думать об управлении состоянием.

Мы будем отслеживать следующие состояния во время операции перетаскивания:

  1. dropDepth
    Это будет целое число. Мы будем использовать его, чтобы отслеживать, на сколько уровней мы находимся в зоне сброса. Позже я объясню это иллюстрацией. ( Спасибо Егору Егорову за то, что он пролил на меня свет! )
  2. inDropZone
    Это будет логическое значение. Мы будем использовать это, чтобы отслеживать, находимся ли мы внутри зоны сброса или нет.
  3. FileList
    Это будет список. Мы будем использовать его для отслеживания файлов, которые были перемещены в зону сброса.

Для обработки состояний React предоставляет useState и useReducer . Мы выберем хук useReducer , учитывая, что мы будем иметь дело с ситуациями, когда состояние зависит от предыдущего состояния.

useReducer принимает редьюсер типа (state, action) => newState и возвращает текущее состояние в паре с методом dispatch .

Вы можете узнать больше о useReducer в документации React.

Внутри компонента App (перед оператором return ) добавьте следующий код:

 ... const reducer = (state, action) => { switch (action.type) { case 'SET_DROP_DEPTH': return { ...state, dropDepth: action.dropDepth } case 'SET_IN_DROP_ZONE': return { ...state, inDropZone: action.inDropZone }; case 'ADD_FILE_TO_LIST': return { ...state, fileList: state.fileList.concat(action.files) }; default: return state; } }; const [data, dispatch] = React.useReducer( reducer, { dropDepth: 0, inDropZone: false, fileList: [] } ) ...

useReducer принимает два аргумента: редьюсер и начальное состояние. Он возвращает текущее состояние и функцию dispatch , с помощью которой можно обновить состояние. Состояние обновляется путем отправки действия, которое содержит type и необязательную полезную нагрузку. Обновление состояния компонента зависит от того, что возвращается оператором case в результате типа действия. (Обратите внимание, что наше начальное состояние — это object .)

Для каждой из переменных состояния мы определили соответствующий оператор case для ее обновления. Обновление выполняется путем вызова функции dispatch , возвращаемой useReducer .

Теперь передайте data и DragAndDrop их в качестве props компоненту dispatch , который у вас есть в файле App.js :

 <DragAndDrop data={data} dispatch={dispatch} />

В верхней части компонента DragAndDrop мы можем получить доступ к обоим значениям из props .

 const { data, dispatch } = props;

Если вы следите за репозиторием, соответствующая ветка — 03-define-reducers .

Давайте закончим логику наших обработчиков событий. Обратите внимание, что многоточие представляет собой две строки:

 e.preventDefault() e.stopPropagation() const handleDragEnter = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth + 1 }); }; const handleDragLeave = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth - 1 }); if (data.dropDepth > 0) return dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }) };

На приведенном ниже рисунке у нас есть вложенные зоны перетаскивания A и B. A — это наша интересующая нас зона. Здесь мы хотим прослушивать события перетаскивания.

Иллюстрация событий ondragenter и ondragleave
Иллюстрация событий ondragenter и ondragleave (большой предварительный просмотр)

При перетаскивании в зону перетаскивания каждый раз, когда мы сталкиваемся с границей, запускается событие ondragenter . Это происходит на границах A-in и B-in . Так как мы входим в зону, мы увеличиваем dropDepth .

Аналогично, при перетаскивании из зоны перетаскивания каждый раз, когда мы сталкиваемся с границей, запускается событие ondragleave . Это происходит на границах A-out и B-out . Поскольку мы покидаем зону, мы уменьшаем значение dropDepth . Обратите внимание, что мы не устанавливаем для inDropZone значение false на границе B-out . Именно поэтому у нас есть эта строка для проверки dropDepth и возврата из функции dropDepth больше 0 .

 if (data.dropDepth > 0) return

Это связано с тем, что даже несмотря на то, что событие ondragleave запускается, мы все еще находимся в зоне A. Только после того, как мы нажмем A-out , а dropDepth теперь равен 0 , мы устанавливаем для inDropZone значение false . На данный момент мы покинули все зоны сброса.

 const handleDragOver = e => { ... e.dataTransfer.dropEffect = 'copy'; dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: true }); };

Каждый раз, когда срабатывает это событие, мы устанавливаем для inDropZone значение true . Это говорит нам о том, что мы находимся внутри зоны сброса. Мы также устанавливаем dropEffect объекта dataTransfer на copy . На Mac это приводит к отображению зеленого знака плюса при перетаскивании элемента в зоне перетаскивания.

 const handleDrop = e => { ... let files = [...e.dataTransfer.files]; if (files && files.length > 0) { const existingFiles = data.fileList.map(f => f.name) files = files.filter(f => !existingFiles.includes(f.name)) dispatch({ type: 'ADD_FILE_TO_LIST', files }); e.dataTransfer.clearData(); dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 }); dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }); } };

Мы можем получить доступ к удаленным файлам с помощью e.dataTransfer.files . Значение представляет собой объект, подобный массиву, поэтому мы используем синтаксис расширения массива, чтобы преобразовать его в массив JavaScript .

Теперь нам нужно проверить, есть ли хотя бы один файл, прежде чем пытаться добавить его в наш массив файлов. Мы также следим за тем, чтобы не включать файлы, которые уже есть в нашем fileList . Объект dataTransfer очищается при подготовке к следующей операции перетаскивания. Мы также сбрасываем значения dropDepth и inDropZone .

Обновите className div в компоненте DragAndDrop . Это условно изменит className div в зависимости от значения data.inDropZone .

 <div className={data.inDropZone ? 'drag-drop-zone inside-drag-area' : 'drag-drop-zone'} ... > <p>Drag files here to upload</p> </div>

Визуализируйте список файлов в App.js путем сопоставления через data.fileList .

 <div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop data={data} dispatch={dispatch} /> <ol className="dropped-files"> {data.fileList.map(f => { return ( <li key={f.name}>{f.name}</li> ) })} </ol> </div>

Теперь попробуйте перетащить несколько файлов в зону перетаскивания. Вы увидите, что когда мы входим в зону перетаскивания, фон становится менее непрозрачным, поскольку активируется класс inside-drag-area .

Когда вы освободите файлы внутри зоны сброса, вы увидите имена файлов, перечисленные под зоной сброса:

Зона перетаскивания с низкой прозрачностью во время перетаскивания
Зона перетаскивания показывает низкую непрозрачность во время перетаскивания (большой предварительный просмотр)
Список файлов, сброшенных в зону сброса
Список файлов, сброшенных в зону перетаскивания (большой превью)

Полная версия этого руководства находится в ветке 04-finish-handlers .

Заключение

Мы увидели, как обрабатывать загрузку файлов в React с помощью API перетаскивания HTML . Мы также узнали, как управлять состоянием с помощью хука useReducer . Мы могли бы расширить функцию file handleDrop . Например, мы могли бы добавить еще одну проверку для ограничения размеров файлов, если бы захотели. Это может произойти до или после проверки существующих файлов. Мы также могли бы сделать зону перетаскивания кликабельной, не затрагивая функциональность перетаскивания.

Ресурсы

  • «Справочник по API хуков: useReducer », React Docs
  • «HTML Drag-and-Drop API», веб-документы MDN
  • «Примеры веб-разработки и XML-разработки с использованием DOM», веб-документы MDN
  • «Как сделать загрузчик файлов с помощью перетаскивания с помощью ванильного JavaScript», Джозеф Циммерман, Smashing Magazine
  • «Простая загрузка файлов перетаскиванием в React», Егор Егоров, Medium