Artigo original: How to Add Drag and Drop in React with React Beautiful DnD
Arrastar e soltar (em inglês, "Drag and Drop") é uma técnica de interação comum, que permite que as pessoas possam intuitivamente mudar as coisas de lugar na página. Pode ter a ver com reordenar uma lista ou até mesmo montar um quebra-cabeças.
Como podemos adicionar esse tipo de interação ao construir um aplicação React com a biblioteca React Beautiful DnD?
- O que é arrastar e soltar?
- O que é o React Beautiful DnD?
- O que vamos construir?
- Passo 0: criar um novo projeto do React.js
- Passo 1: instalar o React Beautiful DnD
- Passo 2: permitir o arrastar e soltar de uma lista com o React Beautiful DnD
- Passo 3: salvar a nova ordem da lista após ter reordenado os itens com o React Beautiful DnD
O que é arrastar e soltar?
Arrastar e soltar é, basicamente, o que o próprio nome indica – uma interação que permite a alguém clicar sobre um item, arrastá-lo e, em seguida, soltá-lo em outro lugar, geralmente causando um efeito colateral no aplicativo.
![react-beautiful-dnd-board](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/react-beautiful-dnd-board.gif)
Esse efeito é bastante comum em aplicativos como listas de tarefas ou painel de gerenciamento de projetos, onde você precisa estabelecer prioridades e criar uma ordem para como as coisas devem ser feitas.
Embora a técnica de "arrastar e soltar" possa ter alguns casos de uso avançados, nos limitaremos à funcionalidade de lista básica em nosso passo a passo.
O que é o React Beautiful DnD?
O React Beautiful DnD é uma biblioteca de arrastar e soltar acessível desenvolvida pela Atlassian. Se você não conhece a Atlassian, eles são a equipe por trás do Jira. Se você não está familiarizado com o Jira, ele é provavelmente a maior ferramenta de gestão ágil na internet atualmente.
Os objetivos da equipe de desenvolvimento da Atlassian eram fornecer recursos de arrastar e soltar tendo a acessibilidade em mente, além de manter a biblioteca enxuta, com um ótimo desempenho e uma API poderosa.
O que vamos construir?
Vamos começar com uma simples lista e adicionar a ela a funcionalidade de arrastar e soltar.
Neste passo a passo, não perderemos tempo construindo a própria lista. A lista que usaremos é composta de uma tag de lista não ordenada padrão (<ul>
) e tags de itens de lista (<li>
), com um pouco de CSS para deixá-los parecidos com cartões.
Vamos nos concentrar em adicionar a funcionalidade de arrastar e soltar para reorganizar a lista usando o React Beautiful DnD.
Passo 0: criar um novo projeto do React.js
Para começar, queremos um aplicativo simples, que inclua uma lista de itens. Pode ser um projeto já existente ou um novo projeto usando seu framework preferido, como, por exemplo, o Create React App.
Comecei com um novo projeto usando o Create React App e adicionei a ele uma lista simples de personagens de Final Space.
![list-of-final-space-characters-1](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/list-of-final-space-characters-1.jpg)
Se você quiser começar do mesmo lugar, pode clonar meu repositório de demonstração na branch específica e começar desde o início comigo.
Este comando clonará a branch específica para começarmos:
git clone --single-branch --branch part-0-starting-point git@github.com:colbyfayock/my-final-space-characters.git
Caso contrário, você pode clonar o repositório normalmente e passar direto para a branch part-0-starting-point
.
Se você quiser acompanhar apenas o código, primeiramente, eu criei um array de objetos:
const finalSpaceCharacters = [
{
id: 'gary',
name: 'Gary Goodspeed',
thumb: '/images/gary.png'
},
...
Então, eu percorri esses objetos com um laço para criar minha lista:
<ul className="characters">
{finalSpaceCharacters.map(({id, name, thumb}) => {
return (
<li key={id}>
<div className="characters-thumb">
<img src={thumb} alt={`${name} Thumb`} />
</div>
<p>
{ name }
</p>
</li>
);
})}
</ul>
Siga junto comigo pelo commit!
Passo 1: instalar o React Beautiful DnD
O primeiro passo é instalar a biblioteca via npm.
De dentro do projeto, execute o seguinte comando:
yarn add react-beautiful-dnd
# or
npm install react-beautiful-dnd --save
Isso adicionará a biblioteca ao projeto e estaremos prontos para usá-la na nossa aplicação.
Passo 2: permitir o arrastar e soltar de uma lista com o React Beautiful DnD
Com a biblioteca instalada, nós podemos dar a nossa lista a habilidade de ser arrastada pela página e largada em outro lugar.
Adicionar o contexto de arrastar e soltar ao nosso app
Na parte superior do arquivo, importe DragDropContext
da biblioteca com:
import { DragDropContext } from 'react-beautiful-dnd';
DragDropContext vai dar ao nosso app a habilidade de utilizar a biblioteca. Funciona de maneira parecida à API Context do React, na qual a biblioteca ganha acesso à árvore de componentes.
Observação: se você planeja adicionar a funcionalidade de arrastar e soltar a mais de uma lista, precisa garantir que o DragDropContext englobe todos esses itens, como, por exemplo, a raiz de sua aplicação. Você não pode aninhar o DragDropContext.
Queremos envolver nossa lista com o DragDropContext:
<DragDropContext>
<ul className="characters">
...
</DragDropContext>
Até aqui, nada terá mudado na aplicação e ela deve carregar como antes.
Permitir arrastar os itens de nossa lista
Em seguida, queremos criar uma área onde podemos arrastar os itens (em inglês, uma área Droppable). Isso nos permitirá prover uma área específica onde nossos itens poderão ser movidos de lugar dentro desse limite.
Primeiramente, adicionamos Droppable
à importação na parte superior do arquivo:
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
Para nosso objetivo, queremos que nossa lista não ordenada inteira (<ul>
) fique dentro da nossa "drop zone" (a zona onde soltaremos os itens). Assim, precisamos envolvê-la com esse componente:
<DragDropContext>
<Droppable droppableId="characters">
{(provided) => (
<ul className="characters">
...
</ul>
)}
</Droppable>
</DragDropContext>
Você notará, porém, que envolvemos a lista de um modo um pouco diferente. Primeiro, definimos um droppableId
para nosso componente <Droppable>
. Isso permite à biblioteca acompanhar essa instância específica entre as interações.
Também estamos criando uma função diretamente naquele componente, que passa o argumento provided
(fornecido, em inglês).
Observação: essa função pode passar dois argumentos, incluindo um argumento snapshot
, mas não o usaremos nesse exemplo.
O argumento provided (texto em inglês) inclui informações e referências ao código de que a biblioteca necessita para funcionar adequadamente.
Para usá-lo, em nosso elemento de lista, adicionamos o seguinte:
<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
Isso criará uma referência (provided.innerRef
) para que a biblioteca acesse o HTML dos elementos da lista. Também aplicará props ao elemento (provided.droppableProps
) que permitem a biblioteca para acompanhar os movimentos e o posicionamento.
Novamente, nesse ponto, ainda não haverá uma funcionalidade perceptível.
Permitir que nossos itens sejam arrastáveis
Agora, vamos para a parte divertida!
A última parte para permitir arrastar e soltar nossos elementos da lista é envolver cada item da lista com um componente de modo semelhante ao que fizemos com toda a lista.
Usaremos o componente Draggable (texto do link em inglês), que, mais uma vez, de modo semelhante ao componente Droppable, incluirá uma função na qual passaremos props aos componentes do item de lista.
Primeiro, precisamos importar Draggable, juntamente com o resto dos componentes.
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
Em seguida, dentro de nosso laço, envolvemos o item de lista retornado com o componente <Draggable />
e sua função de nível superior.
{finalSpaceCharacters.map(({id, name, thumb}) => {
return (
<Draggable>
{(provided) => (
<li key={id}>
...
</li>
)}
</Draggable>
Como agora temos um novo componente de nível superior em nosso laço, vamos mover a prop key
do elemento da lista para Draggable:
{finalSpaceCharacters.map(({id, name, thumb}) => {
return (
<Draggable key={id}>
{(provided) => (
<li>
Também precisamos definir duas props adicionais em <Draggable>
, um draggableId
e um index
.
Queremos adicionar esse index
(índice, em inglês) como um argumento para nossa função map
e incluir essas props ao nosso componente:
{finalSpaceCharacters.map(({id, name, thumb}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
Por fim, precisamos definir algumas props na própria lista de elementos.
No nosso elemento de lista, adicionaremos essa ref
e faremos o spreading das props adicionais do argumento provided
:
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
Depois, se atualizarmos a página e passarmos o mouse por nossos itens da lista, veremos que é possível arrastá-los!
![drag-items-revert-state-1](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/drag-items-revert-state-1.gif)
Entretanto, você perceberá que, ao começar a mover um item, a parte inferior da página parecerá um pouco estranha. Parece haver uma questão com relação ao vazamento (em inglês, overflowing) dos itens da lista sobre o rodapé.
Você também perceberá que, no console do desenvolvedor, o React Beautiful DnD está nos passando uma mensagem de aviso, dizendo que está faltando algo, chamado placeholder.
![react-beautiful-dnd-warning-placeholder](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/react-beautiful-dnd-warning-placeholder.jpg)
Adicionar o placeholder do React Beautiful DnD
Parte dos requisitos do React Beautiful DnD é a adição de um item chamado placeholder.
Isso é algo fornecido na instalação e é usado para preencher o espaço ocupado anteriormente pelo item que agora estamos arrastando.
Para adicioná-lo, incluiremos provided.placeholder
na parte inferior de nosso componente de lista de nível superior "Droppable" - em nosso caso, na parte inferior do elemento <ul>
:
<Droppable droppableId="characters">
{(provided) => (
<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
...
{provided.placeholder}
</ul>
)}
</Droppable>
Agora, se começarmos a arrastar os itens em nosso navegador, veremos que o fluxo da página não terá mais problemas e que o conteúdo ficará onde deve estar!
![drag-items-revert-fixed-spacing](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/drag-items-revert-fixed-spacing.gif)
O último problema, no entanto, está no fato de, ao mover algum dos itens, ele não permanecer no lugar em que o soltamos. Como, então, salvar a ordem dos nossos itens?
Siga junto comigo pelo commit!
Passo 3: salvar a nova ordem da lista após ter reordenado os itens com o React Beautiful DnD
Quando movemos nossos itens, eles permanecem onde estão por uma fração de segundo e, assim que o React Beautiful DnD termina de fazer seu trabalho, a árvore de componentes renderizará novamente.
Depois da nova renderização dos componentes, os itens retornam ao mesmo lugar onde estavam, pois nunca foram salvos como estavam fora da memória do DnD.
Para resolver isso, o DragDropContext
recebe uma prop onDragEnd
, que nos permitirá disparar uma função após termos concluído o processo de arrastar. A função passa argumentos que incluem a nova ordem dos nossos itens para podermos atualizar nosso estado para o próximo ciclo de renderização.
Salvar nossos itens da lista no state
Primeiro, vamos armazenar nossos itens no state, para termos algo para atualizar entre os ciclos.
Na parte superior do arquivo, adicionamos useState
ao import do React:
import React, { useState } from 'react';
Em seguida, criaremos nosso state usando nossa lista de itens padrão.
Adicione o seguinte à parte superior do nosso componente App:
const [characters, updateCharacters] = useState(finalSpaceCharacters);
Como atualizaremos nosso novo state, characters
, fornecendo nossos itens da lista e sua ordem, queremos substituir o array que mapeamos inicialmente por nosso novo state:
<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
{characters(({id, name, thumb}, index) => {
Se salvarmos e atualizarmos nossa página, nada deve mudar!
Atualizar o state ao arrastar os itens
Agora que temos nosso state, podemos atualizar o state sempre que itens da lista forem arrastados.
O componente DragDropContext
que adicionamos à nossa página recebe uma prop onDragEnd
. Conforme o nome diz (ao final do processo de arrastar, em inglês), essa prop disparará uma função sempre que alguém parar de arrastar um item na lista.
Vamos adicionar uma função handleOnDragEnd
como nossa prop:
<DragDropContext onDragEnd={handleOnDragEnd}>
Em seguida, precisamos que aquela função exista de fato.
Podemos definir uma função em nosso state:
function handleOnDragEnd(result) {
}
Nossa função recebe um argumento chamado result
.
Se adicionarmos console.log(result)
à função e movermos um item em nossa lista, veremos que ela inclui detalhes sobre o que deve ser o state atualizado após nossa ação de movimentação.
![react-beautiful-dnd-ondragend-result](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/react-beautiful-dnd-ondragend-result.jpg)
Especificamente, queremos usar o valor de index
nas propriedades destination
e source
, que nos informam o índice do item que está sendo movido e qual deve ser o novo índice daquele item no array de itens.
Usando-o, adicionamos o seguinte à nossa função:
const items = Array.from(characters);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
updateCharacters(items);
O que estamos fazendo é:
- Criar uma cópia de nosso array
characters
- Usar o valor
source.index
para encontrar nosso item do novo array e removê-lo usando o métodosplice
- O resultado é desestruturado, assim, temos um novo objeto
reorderedItem
, que é o nosso item arrastado - Usar o
destination.index
para adicionar o item novamente no array, mas em seu novo local, usandosplice
novamente - Por fim, atualizar o state
characters
com a funçãoupdateCharacters
. Agora, após salvar nossa função, podemos mover nossos personagens e eles são salvos no novo local!
![drag-drop-save-state](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/drag-drop-save-state.gif)
Evitando o erro de arrastar para fora dos limites
Uma pequena questão a resolver com nossa implementação é o que ocorre quando o item não é arrastado exatamente para dentro do contêiner definido. Quando isso ocorre, temos um erro.
![drag-out-of-container-error](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/drag-out-of-container-error.gif)
O problema é que, ao fazermos isso, não temos um local de destino.
Para evitar isso, podemos simplesmente adicionar uma instrução acima do código que move nosso item para verificar se o destino existe. Se não existir, saímos da função:
function handleOnDragEnd(result) {
if (!result.destination) return;
Se recarregarmos a página e tentarmos arrastar nosso item para fora do contêiner novamente, ele retornará ao local original sem o erro!
![drag-out-of-container-snap-back](https://www.freecodecamp.org/portuguese/news/content/images/2022/03/drag-out-of-container-snap-back.gif)
Siga junto comigo pelo commit!
O que mais podemos fazer com o React Beautiful DnD?
Estilos personalizados ao arrastar e soltar
Ao mover os itens, o DnD fornecerá um snapshot do state dado. Com essas informações, podemos aplicar estilos personalizados, de modo que, quando movermos nossos itens, poderemos mostrar um state ativo para a lista, para o item que movemos, ou ambos!
https://react-beautiful-dnd.netlify.app/?path=/story/single-vertical-list--basic
Arrastar entre listas diferentes
Se você já usou o Trello anteriormente, ou uma ferramenta semelhante, deve estar familiarizado com o conceito de colunas diferentes entre as quais você pode arrastar cartões, de modo a priorizar e organizar suas tarefas e ideias.
Esta biblioteca permite a você fazer isso, dando a capacidade de arrastar e soltar itens de uma área dentro da qual um item pode ser arrastado para outra área do mesmo tipo.
https://react-beautiful-dnd.netlify.app/?path=/story/multiple-vertical-lists--stress-test