Cómo usar la API HTML de arrastrar y soltar en React

Publicado: 2022-03-10
Resumen rápido ↬ En este tutorial, crearemos un componente de arrastrar y soltar de React para cargar archivos e imágenes. En el proceso, aprenderemos sobre la API de arrastrar y soltar de HTML. También aprenderemos a usar el enlace useReducer para administrar el estado en un componente funcional de React.

La 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 .

  1. El evento dragenter se activa cuando un elemento arrastrado ingresa a un destino de colocación válido.
  2. El evento dragleave se activa cuando un elemento arrastrado deja un destino de colocación válido.
  3. 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).
  4. 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.

¡Más después del salto! Continúe leyendo a continuación ↓

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.

Zona de descenso
div para convertirlo en una zona de colocación (vista previa grande)

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:

  1. 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! )
  2. inDropZone
    Este será un booleano. Usaremos esto para realizar un seguimiento de si estamos dentro de la zona de caída o no.
  3. 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.

Una ilustración de los eventos ondragenter y ondragleave
Una ilustración de los eventos ondragenter y ondragleave (vista previa grande)

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:

Zona de colocación que muestra baja opacidad durante el arrastre
Zona de colocación que muestra baja opacidad durante el arrastre (vista previa grande)
Una lista de archivos colocados en la zona de colocación
Una lista de archivos colocados en la zona de colocación (vista previa grande)

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