Como usar a API HTML de arrastar e soltar no React
Publicados: 2022-03-10A API de arrastar e soltar é um dos recursos mais legais do HTML. Ele nos ajuda a implementar recursos de arrastar e soltar em navegadores da web.
No contexto atual, estaremos arrastando arquivos de fora do navegador. Ao soltar o(s) arquivo(s), nós os colocamos em uma lista e exibimos seus nomes. Com os arquivos em mãos, poderíamos então realizar alguma outra operação no(s) arquivo(s), por exemplo, carregá-los em um servidor em nuvem.
Neste tutorial, focaremos em como implementar a ação de arrastar e soltar em um aplicativo React. Se o que você precisa é de uma implementação simples de JavaScript
, talvez você queira primeiro ler “Como fazer um uploader de arquivos arrastar e soltar com JavaScript Vanilla”, um excelente tutorial escrito por Joseph Zimmerman não muito tempo atrás.
Os dragenter
, dragleave
, dragover
e drop
Existem oito eventos diferentes de arrastar e soltar. Cada um é acionado em um estágio diferente da operação de arrastar e soltar. Neste tutorial, vamos nos concentrar nos quatro que são disparados quando um item é solto em uma zona de soltar: dragenter
, dragleave
, dragover
e drop
.
- O evento
dragenter
acionado quando um item arrastado entra em um destino de soltar válido. - O evento
dragleave
acionado quando um item arrastado deixa um destino de soltar válido. - O evento
dragover
acionado quando um item arrastado está sendo arrastado sobre um destino de soltar válido. (Ele dispara a cada poucas centenas de milissegundos.) - O evento
drop
é acionado quando um item cai em um destino de soltar válido, ou seja, arrastado e solto.
Podemos transformar qualquer elemento HTML em um destino de soltar válido definindo os atributos do manipulador de eventos ondragover
e ondrop
.
Você pode aprender tudo sobre os oito eventos nos documentos da web do MDN.
Eventos de arrastar e soltar no React
Para começar, clone o repositório do tutorial a partir deste URL:
https://github.com/chidimo/react-dnd.git
Confira a filial 01-start
. Certifique-se de ter o yarn
instalado também. Você pode obtê-lo em yarnpkg.com.
Mas se preferir, crie um novo projeto React e substitua o conteúdo do App.js pelo código abaixo:
import React from 'react'; import './App.css'; function App() { return ( <div className="App"> <h1>React drag-and-drop component</h1> </div> ); } export default App;
Além disso, substitua o conteúdo de App.css pelo estilo CSS abaixo:
.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; }
Se você clonou o repositório, emita os seguintes comandos (em ordem) para iniciar o aplicativo:
yarn # install dependencies yarn start # start the app
A próxima etapa é criar um componente de arrastar e soltar. Crie um arquivo DragAndDrop.js dentro da pasta src/
. Digite a seguinte função dentro do arquivo:
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;
No return div
, definimos nossos atributos de manipulador de eventos HTML
de foco. Você pode ver que a única diferença do HTML
puro é a carcaça do camelo.
O div
agora é um destino de soltar válido, pois definimos os atributos do manipulador de eventos onDragOver
e onDrop
.
Também definimos funções para lidar com esses eventos. Cada uma dessas funções de manipulador recebe o objeto de evento como seu argumento.
Para cada um dos manipuladores de eventos, chamamos preventDefault()
para impedir que o navegador execute seu comportamento padrão. O comportamento padrão do navegador é abrir o arquivo descartado. Também chamamos stopPropagation()
para garantir que o evento não seja propagado de elementos filho para pai.
Importe o componente DragAndDrop
para o componente App
e renderize-o abaixo do título.
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop /> </div>
Agora visualize o componente no navegador e você deverá ver algo como a imagem abaixo.
Se você está seguindo com o repo, o branch correspondente é 02-start-dragndrop
Gerenciando o estado com o gancho useReducer
Nosso próximo passo será escrever a lógica para cada um de nossos manipuladores de eventos. Antes de fazermos isso, temos que considerar como pretendemos acompanhar os arquivos descartados. É aqui que começamos a pensar na gestão do Estado.
Estaremos acompanhando os seguintes estados durante a operação de arrastar e soltar:
-
dropDepth
Este será um número inteiro. Vamos usá-lo para acompanhar quantos níveis de profundidade estamos na zona de queda. Mais tarde, explicarei isso com uma ilustração. ( Créditos a Egor Egorov por lançar uma luz sobre isso para mim! ) -
inDropZone
Este será um booleano. Usaremos isso para acompanhar se estamos dentro da zona de lançamento ou não. -
FileList
Esta será uma lista. Vamos usá-lo para acompanhar os arquivos que foram soltos na zona de lançamento.
Para lidar com os estados, o React fornece os ganchos useState
e useReducer
. useReducer
pelo gancho useReducer, pois estaremos lidando com situações em que um estado depende do estado anterior.
O gancho useReducer
aceita um redutor do tipo (state, action) => newState
e retorna o estado atual emparelhado com um método dispatch
.
Você pode ler mais sobre useReducer
nos documentos do React.
Dentro do componente App
(antes da instrução return
), adicione o seguinte 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: [] } ) ...
O gancho useReducer
aceita dois argumentos: um redutor e um estado inicial. Ele retorna o estado atual e uma função de dispatch
com a qual atualizar o estado. O estado é atualizado despachando uma ação que contém um type
e uma carga útil opcional. A atualização feita no estado do componente depende do que é retornado da instrução case como resultado do tipo de ação. (Observe aqui que nosso estado inicial é um object
.)
Para cada uma das variáveis de estado, definimos uma instrução case correspondente para atualizá-la. A atualização é realizada invocando a função de dispatch
retornada por useReducer
.
Agora passe data
e dispatch
como props
para o componente DragAndDrop
que você tem em seu arquivo App.js :
<DragAndDrop data={data} dispatch={dispatch} />
Na parte superior do componente DragAndDrop
, podemos acessar os dois valores de props
.
const { data, dispatch } = props;
Se você está seguindo com o repo, o branch correspondente é 03-define-reducers
.
Vamos terminar a lógica de nossos manipuladores de eventos. Observe que as reticências representam as duas linhas:
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 }) };
Na ilustração a seguir, aninhamos as zonas para soltar A e B. A é nossa zona de interesse. É aqui que queremos ouvir eventos de arrastar e soltar.
Ao arrastar para uma zona de soltar, cada vez que atingimos um limite, o evento ondragenter
é acionado. Isso acontece nos limites A-in
e B-in
. Como estamos entrando na zona, incrementamos dropDepth
.
Da mesma forma, ao arrastar para fora de uma zona de soltar, cada vez que atingimos um limite, o evento ondragleave
é acionado. Isso acontece nos limites A-out
e B-out
. Como estamos saindo da zona, diminuímos o valor de dropDepth
. Observe que não definimos inDropZone
como false
no limite B-out
. Por isso temos esta linha para verificar o dropDepth e retornar da função dropDepth
maior que 0
.
if (data.dropDepth > 0) return
Isso ocorre porque, mesmo que o evento ondragleave
seja disparado, ainda estamos dentro da zona A. É somente depois de acertarmos A-out
e dropDepth
agora é 0
que definimos inDropZone
como false
. Neste ponto, deixamos todas as zonas de lançamento.
const handleDragOver = e => { ... e.dataTransfer.dropEffect = 'copy'; dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: true }); };
Cada vez que esse evento é acionado, definimos inDropZone
como true
. Isso nos diz que estamos dentro da zona de lançamento. Também definimos o dropEffect
no objeto dataTransfer
para copy
. Em um Mac, isso tem o efeito de mostrar um sinal de mais verde conforme você arrasta um item na zona de soltar.
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 acessar os arquivos descartados com e.dataTransfer.files
. O valor é um objeto semelhante a um array, então usamos a sintaxe de dispersão de array para convertê-lo em um array JavaScript
.
Agora precisamos verificar se há pelo menos um arquivo antes de tentar adicioná-lo ao nosso array de arquivos. Também nos certificamos de não incluir arquivos que já estão em nossa fileList
. O objeto dataTransfer
é limpo em preparação para a próxima operação de arrastar e soltar. Também redefinimos os valores de dropDepth
e inDropZone
.
Atualize o className
do div
no componente DragAndDrop
. Isso alterará condicionalmente o className
da div
dependendo do 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>
Renderize a lista de arquivos em App.js mapeando por meio 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>
Agora tente arrastar e soltar alguns arquivos na zona de soltar. Você verá que à medida que entramos na zona de soltar, o plano de fundo se torna menos opaco porque a classe inside-drag-area
é ativada.
Ao liberar os arquivos dentro da zona para soltar, você verá os nomes dos arquivos listados na zona para soltar:
A versão completa deste tutorial está no branch 04-finish-handlers
.
Conclusão
Vimos como lidar com uploads de arquivos no React usando a API HTML
de arrastar e soltar. Também aprendemos como gerenciar o estado com o gancho useReducer
. Poderíamos estender a função handleDrop
do arquivo. Por exemplo, poderíamos adicionar outra verificação para limitar os tamanhos dos arquivos, se quiséssemos. Isso pode ocorrer antes ou depois da verificação de arquivos existentes. Também poderíamos tornar a zona de soltar clicável sem afetar a funcionalidade de arrastar e soltar.
Recursos
- “Referência da API Hooks:
useReducer
,” React Docs - “API de arrastar e soltar HTML”, documentos da web MDN
- “Exemplos de desenvolvimento Web e XML usando o DOM”, documentos da web MDN
- “Como fazer um uploader de arquivos de arrastar e soltar com Vanilla JavaScript”, Joseph Zimmerman, Smashing Magazine
- “Carregamento de arquivo simples de arrastar e soltar no React”, Egor Egorov, Medium