Configurando a biblioteca React Beautiful Drag and Drop
https://www.lucasmantuan.com.br
https://github.com/lucasmantuan
https://www.linkedin.com/in/lucasmantuan
1. Começando
Com um projeto iniciado, instalamos o pacote react-beautiful-dnd
. Ele possui três componentes que importamos com o seguinte comando:
import { DragDropContext, Draggable, Droppable }
from "react-beautiful-dnd";
O DragDropContext
funciona como um container onde especificamos a área que vamos utilizar para renderizar os outros dois componentes.
Ele possui alguns eventos que podemos utilizar, para isso passamos uma callback para o evento. Por exemplo, o evento onDragStart
é chamado quando iniciamos o arrasto de um item, o onDragUpdate
, quando temos alguma atualização no momento que o item é arrastado. Temos também o evento onDragEnd
que é ==obrigatório== e é chamado quando o arrasto termina.
function App() {
const [ state, dispatch ] = useReducer(reducer, { tasks });
const onDragEnd = useCallback((result) => { }, []);
return (
<div>
<DragDropContext
onDragEnd={ onDragEnd }>
</DragDropContext>
</div>
);
};
export default App;
A callback passada em onDragEnd
recebe em result
um objeto com diversas informações sobre o item arrastado, como a origem, o destino, se o usuário cancelou o arrasto, etc.
Utilizamos o hook
useCallback
para evitar renderizações desnecessárias e o hookuseReducer
para otimizar o gerenciamento quando tivermos mais de uma lista.
2. Droppable
O componente Droppable
é o componente que cria a área onde podemos soltar os itens, ele recebe como parâmetro um id em droppableId
e um tipo em type
. O id é obrigatório e deve ser único para cada Droppable
que adicionarmos e o type
nós utilizamos para limitar quais itens podem ser aceitos dentro de um Droppable
.
Por exemplo, se tivéssemos outro Droppable
com o mesmo type
, poderíamos arrastar os itens entre os dois componentes.
O componente não cria um elemento HTML, ele espera que o children
seja uma função que retorne um componente. Este componente que a função retorna recebe uma ref
e droppableProps
que por sua vez são repassados por provided
.
<Droppable
droppableId="tasks"
type="TODO">
{ (provided) => {
return (
<div
ref={ provided.innerRef }
{ ...provided.droppableProps }>
{ provided.placeholder }
</div>
);
} }
</Droppable>
Além de provided
a função pode receber o snapshot
como parâmetro, que é um objeto com algumas propriedades que podemos utilizar para estilizar nosso componente.
Por exemplo, se o usuário estiver arrastando um item sobre o Droppable
, a propriedade isDraggingOver
será definida como true
, o que permitiria alterar o fundo da lista.
Por último, precisamos utilizar o componente placeholder
que cria um espaço para quando um item estiver sendo arrastado.
3. Draggable
Agora precisamos criar nossa lista de itens arrastáveis. Em nosso exemplo vamos criar ela dentro de Droppable
pois vamos arrastar os itens dentro da própria lista.
É importante que nossa estrutura de dados tenha um id único, pois é com este id que vamos identificar os Draggable
dentro da propriedade draggableId
. O index
é utilizado para termos a ordem em que os itens estão organizados dentro da lista.
const data = [
{ id: "1a2b", title: "Primeira Tarefa" },
{ id: "3c4d", title: "Segunda Tarefa" }
];
Assim como em Droppable
criamos uma função que vai retornar o elemento HTML arrastável, que por sua vez recebe da função uma innerRef
e as props draggableProps
e dragHandleProps
.
{ state.tasks?.map((task, index) => {
return (
<Draggable
draggableId={ task.id }
index={ index }
key={ task.id }>
{ (provided) => {
return (
<div
ref={ provided.innerRef }
{ ...provided.draggableProps }
{ ...provided.dragHandleProps }>
<div>
{ task.title }
</div>
</div>
);
} }
</Draggable>
);
}) }
Podemos receber também a propriedade isDragging
do snapshot
para, por exemplo, indicar se o item está sendo arrastado e com isso estilizar o componente.
4. Arrastar e Soltar
Para arrastarmos os itens, primeiro verificamos em nossa callback onDragEnd
se houve ou não um cancelamento do usuário na propriedade destination
do objeto result
, se sim, não fazemos nada.
Caso contrário, fazemos o dispatch
enviando os ids e índices do item arrastado.
const onDragEnd = useCallback((result) => {
if (result.reason === "DROP") return;
dispatch({
type: "MOVE",
from: result.source.droppableId,
to: result.destination.droppableId,
fromIndex: result.source.index,
toIndex: result.destination.index,
});
}, []);
Depois verificamos em nosso reducer
se temos algo em action.to
e se action.from
e action.to
são iguais, com isso sabemos se o item foi arrastado para o mesmo local e, também não fazemos nada.
Senão, fazemos uma cópia da lista para manter a imutabilidade da lista original, o que garante uma renderização mais eficiente, e removemos o item que desejamos mover com o método splice
e também com o método splice
inserimos o item no local correto. Se desejarmos, poderíamos chamar em nosso reducer
uma função para persistirmos os dados.
const reducer = (state, action) => {
switch (action.type) {
case "MOVE": {
if (!action.to) return;
if (action.to === action.from &&
action.toIndex === action.fromIndex) return;
const newState = JSON.parse(JSON.stringify(state.data));
const [ removeItem ] = newState.splice(action.fromIndex, 1);
newState.splice(action.toIndex, 0, removeItem);
return { data: newState };
}
};
};
O método
splice
adiciona novos elementos em um array enquanto remove os antigos.
5. Código Final
import { useCallback, useReducer }
from "react";
import { DragDropContext, Draggable, Droppable }
from "react-beautiful-dnd";
const tasks = [
{ id: "1a2b", title: "Primeira Tarefa" },
{ id: "3c4d", title: "Segunda Tarefa" }
];
const containerStyle = {
border: "1px solid lightgrey",
borderRadius: "2px",
fontFamily: "monospace",
marginBottom: "8px",
padding: "8px"
};
const itemStyle = {
border: "1px solid lightgrey",
borderRadius: "2px",
fontFamily: "monospace",
margin: "8px",
padding: "8px"
};
function App() {
const reducer = (state, action) => {
switch (action.type) {
case "MOVE": {
if (!action.to) return;
if (action.to === action.from
&& action.toIndex === action.fromIndex) return;
const newState = JSON.parse(JSON.stringify(state.tasks));
const [ removeItem ] = newState.splice(action.fromIndex, 1);
newState.splice(action.toIndex, 0, removeItem);
return { tasks: newState };
}
};
};
const [ state, dispatch ] = useReducer(reducer, { tasks });
const onDragEnd = useCallback((result) => {
if (result.reason === "DROP") {
dispatch({
type: "MOVE",
from: result.source.droppableId,
to: result.destination.droppableId,
fromIndex: result.source.index,
toIndex: result.destination.index,
});
}
}, []);
const DraggableItem = ({ provided, task }) => {
return (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}>
<div style={itemStyle}>{task.title}</div>
</div>
);
};
const DroppableContainer = ({ provided, tasks }) => {
return (
<div
ref={provided.innerRef}
{...provided.droppableProps}>
{tasks?.map((task, index) => (
<DraggableItem
key={task.id}
provided={provided}
task={task}
index={index}
/>
))}
{provided.placeholder}
</div>
);
};
return (
<div style={containerStyle}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable
droppableId='tasks'
type='TODO'>
{(provided) => (
<DroppableContainer
provided={provided}
tasks={state.tasks}
/>
)}
</Droppable>
</DragDropContext>
</div>
);
};
export default App;