Cómo usar la API HTML de arrastrar y soltar en React
Publicado: 2022-03-10La API de arrastrar y soltar es una de las mejores características de HTML. Nos ayuda a implementar funciones de arrastrar y soltar en los navegadores web.
En el contexto actual, arrastraremos archivos desde fuera del navegador. Al colocar los archivos, los colocamos en una lista y mostramos sus nombres. Con los archivos en la mano, podríamos realizar alguna otra operación en los archivos, por ejemplo, subirlos a un servidor en la nube.
En este tutorial, nos centraremos en cómo implementar la acción de arrastrar y soltar en una aplicación React. Si lo que necesita es una implementación simple de JavaScript
, tal vez primero le gustaría leer "Cómo hacer un cargador de archivos de arrastrar y soltar con Vanilla JavaScript", un excelente tutorial escrito por Joseph Zimmerman no hace mucho tiempo.
Los dragenter
dragenter, dragleave
, dragover
y drop
Hay ocho eventos diferentes de arrastrar y soltar. Cada uno dispara en una etapa diferente de la operación de arrastrar y soltar. En este tutorial, nos centraremos en los cuatro que se activan cuando se suelta un elemento en una zona de colocación: dragenter
, dragleave
, dragover
y drop
.
- El evento
dragenter
se activa cuando un elemento arrastrado ingresa a un destino de colocación válido. - El evento
dragleave
se activa cuando un elemento arrastrado deja un destino de colocación válido. - El evento de
dragover
se activa cuando un elemento arrastrado se arrastra sobre un destino de colocación válido. (Se dispara cada pocos cientos de milisegundos). - El evento
drop
se activa cuando un elemento cae sobre un objetivo de soltar válido, es decir, se arrastra y se suelta.
Podemos convertir cualquier elemento HTML en un objetivo de colocación válido definiendo los atributos del controlador de eventos ondragover
y ondrop
.
Puede aprender todo sobre los ocho eventos en los documentos web de MDN.
Eventos de arrastrar y soltar en React
Para comenzar, clone el repositorio del tutorial desde esta URL:
https://github.com/chidimo/react-dnd.git
Echa un vistazo a la sucursal 01-start
. Asegúrate de tener yarn
instalado también. Puede obtenerlo en yarnpkg.com.
Pero si lo prefiere, cree un nuevo proyecto React y reemplace el contenido de App.js con el siguiente código:
import React from 'react'; import './App.css'; function App() { return ( <div className="App"> <h1>React drag-and-drop component</h1> </div> ); } export default App;
Además, reemplace el contenido de App.css con el siguiente estilo 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; }
Si clonó el repositorio, emita los siguientes comandos (en orden) para iniciar la aplicación:
yarn # install dependencies yarn start # start the app
El siguiente paso es crear un componente de arrastrar y soltar. Cree un archivo DragAndDrop.js dentro de la carpeta src/
. Introduzca la siguiente función dentro del archivo:
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;
En el div
de retorno, hemos definido nuestros atributos de controlador de eventos HTML
de enfoque. Puede ver que la única diferencia con el HTML
puro es la carcasa de camello.
El div
ahora es un destino de colocación válido ya que hemos definido los atributos del controlador de eventos onDragOver
y onDrop
.
También definimos funciones para manejar esos eventos. Cada una de estas funciones de controlador recibe el objeto de evento como su argumento.
Para cada uno de los controladores de eventos, llamamos a preventDefault()
para evitar que el navegador ejecute su comportamiento predeterminado. El comportamiento predeterminado del navegador es abrir el archivo soltado. También llamamos a stopPropagation()
para asegurarnos de que el evento no se propague de elementos secundarios a elementos principales.
Importe el componente DragAndDrop
en el componente App
y reprodúzcalo debajo del encabezado.
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop /> </div>
Ahora vea el componente en el navegador y debería ver algo como la imagen de abajo.
Si sigue con el repositorio, la rama correspondiente es 02-start-dragndrop
Administrar el estado con el gancho useReducer
Nuestro siguiente paso será escribir la lógica para cada uno de nuestros controladores de eventos. Antes de hacer eso, debemos considerar cómo pretendemos realizar un seguimiento de los archivos eliminados. Aquí es donde empezamos a pensar en la gestión estatal.
Realizaremos un seguimiento de los siguientes estados durante la operación de arrastrar y soltar:
-
dropDepth
Este será un número entero. Lo usaremos para realizar un seguimiento de cuántos niveles de profundidad estamos en la zona de caída. Más adelante explicaré esto con una ilustración. ( ¡Créditos a Egor Egorov por iluminarme en este caso! ) -
inDropZone
Este será un booleano. Usaremos esto para realizar un seguimiento de si estamos dentro de la zona de caída o no. -
FileList
Esta será una lista. Lo usaremos para realizar un seguimiento de los archivos que se han colocado en la zona de colocación.
Para manejar estados, React proporciona los ganchos useState
y useReducer
. Optaremos por el gancho useReducer
dado que nos ocuparemos de situaciones en las que un estado depende del estado anterior.
El useReducer
acepta un reductor de tipo (state, action) => newState
y devuelve el estado actual junto con un método de dispatch
.
Puede leer más sobre useReducer
en los documentos de React.
Dentro del componente de la App
(antes de la declaración de return
), agregue el siguiente código:
... 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: [] } ) ...
El gancho useReducer
acepta dos argumentos: un reductor y un estado inicial. Devuelve el estado actual y una función de dispatch
con la que actualizar el estado. El estado se actualiza enviando una acción que contiene un type
y una carga útil opcional. La actualización realizada en el estado del componente depende de lo que se devuelva de la declaración del caso como resultado del tipo de acción. (Tenga en cuenta que nuestro estado inicial es un object
).
Para cada una de las variables de estado, definimos una declaración de caso correspondiente para actualizarla. La actualización se realiza invocando la función de dispatch
devuelta por useReducer
.
Ahora pase data
y dispatch
como props
al componente DragAndDrop
que tiene en su archivo App.js :
<DragAndDrop data={data} dispatch={dispatch} />
En la parte superior del componente DragAndDrop
, podemos acceder a ambos valores desde props
.
const { data, dispatch } = props;
Si sigue con el repositorio, la rama correspondiente es 03-define-reducers
.
Terminemos la lógica de nuestros controladores de eventos. Tenga en cuenta que los puntos suspensivos representan las dos líneas:
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 }) };
En la siguiente ilustración, hemos anidado las zonas de colocación A y B. A es nuestra zona de interés. Aquí es donde queremos escuchar los eventos de arrastrar y soltar.
Al arrastrar a una zona de colocación, cada vez que alcanzamos un límite, se activa el evento ondragenter
. Esto sucede en los límites A-in
y B-in
. Como estamos ingresando a la zona, incrementamos dropDepth
.
Del mismo modo, al arrastrar fuera de una zona de colocación, cada vez que alcanzamos un límite, se activa el evento ondragleave
. Esto sucede en los límites A-out
y B-out
. Como estamos saliendo de la zona, disminuimos el valor de dropDepth
. Tenga en cuenta que no configuramos inDropZone
en false
en el límite B-out
. Es por eso que tenemos esta línea para verificar dropDepth y devolver desde la función dropDepth
mayor que 0
.
if (data.dropDepth > 0) return
Esto se debe a que a pesar de que se activa el evento ondragleave
, todavía estamos dentro de la zona A. Solo después de haber presionado A-out
y dropDepth
ahora es 0
, establecemos inDropZone
en false
. En este punto, hemos dejado todas las zonas de caída.
const handleDragOver = e => { ... e.dataTransfer.dropEffect = 'copy'; dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: true }); };
Cada vez que se activa este evento, establecemos inDropZone
en true
. Esto nos dice que estamos dentro de la zona de lanzamiento. También configuramos dropEffect
en el objeto dataTransfer
para copy
. En una Mac, esto tiene el efecto de mostrar un signo más verde a medida que arrastra un elemento en la zona de colocación.
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 }); } };
Podemos acceder a los archivos soltados con e.dataTransfer.files
. El valor es un objeto similar a una matriz, por lo que usamos la sintaxis de distribución de matriz para convertirlo en una matriz de JavaScript
.
Ahora debemos verificar si hay al menos un archivo antes de intentar agregarlo a nuestra matriz de archivos. También nos aseguramos de no incluir archivos que ya están en nuestra lista de fileList
. El objeto dataTransfer
se borra como preparación para la siguiente operación de arrastrar y soltar. También restablecemos los valores de dropDepth
e inDropZone
.
Actualice el className
del div
en el componente DragAndDrop
. Esto cambiará condicionalmente el className
del div
según el valor de data.inDropZone
.
<div className={data.inDropZone ? 'drag-drop-zone inside-drag-area' : 'drag-drop-zone'} ... > <p>Drag files here to upload</p> </div>
Representa la lista de archivos en App.js mediante la asignación a través de 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>
Ahora intente arrastrar y soltar algunos archivos en la zona de colocación. Verá que a medida que ingresamos a la zona de colocación, el fondo se vuelve menos opaco porque la clase inside-drag-area
está activada.
Cuando libere los archivos dentro de la zona de colocación, verá los nombres de archivo enumerados debajo de la zona de colocación:
La versión completa de este tutorial se encuentra en la rama 04-finish-handlers
.
Conclusión
Hemos visto cómo manejar las cargas de archivos en React usando la API HTML
de arrastrar y soltar. También aprendimos cómo administrar el estado con el gancho useReducer
. Podríamos extender la función file handleDrop
. Por ejemplo, podríamos agregar otra verificación para limitar el tamaño de los archivos si quisiéramos. Esto puede venir antes o después de la verificación de archivos existentes. También podríamos hacer clic en la zona de colocación sin afectar la funcionalidad de arrastrar y soltar.
Recursos
- "Referencia de la API de ganchos:
useReducer
", React Docs - "API de arrastrar y soltar HTML", documentos web de MDN
- “Ejemplos de desarrollo web y XML utilizando el DOM”, documentos web de MDN
- "Cómo hacer un cargador de archivos de arrastrar y soltar con Vanilla JavaScript", Joseph Zimmerman, Smashing Magazine
- "Carga de archivos simple de arrastrar y soltar en React", Egor Egorov, Medium