Drag & Drop
En esta sección realizaremos un Drag&Drop, para ello usaremos una librería externa. React hello-pangea/dnd es una librería que nos permite utilizar drag and drop en React. Esta diseñada para ser liviana, rápida y fácil de usar.
Recursos
En la sección usaremos los siguientes recursos:
- hello-pangea/dnd
- documentación api
- freecodecamp tutorial
- forwarding-refs
- Tutorial: Yoelvis Mulen { code }
Instalación
Instalamos la librería hello-pangea/dnd.
# npm
npm install @hello-pangea/dnd --save
Proyecto inicial
Para este ejemplo vamos a usar un proyecto de simple, ordenar un array con varias tareas.
Código inicial del App.jsx
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function App() {
const initialTodos = [
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Aprender Js" },
{ id: 3, text: "Aprender Vue" },
];
const [todos, setTodos] = useState(initialTodos);
return (
<div className="container">
<h1>Drag & Drop</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
export default App;
index.css
li {
border: 2px solid rgb(114, 168, 250);
padding: 0.5rem;
}
ul {
list-style: none;
padding: 1rem;
border: 2px solid rgb(30, 72, 210);
}
Importamos los componentes de la librería
Lo primero que tenemos que hacer es importar los componentes que vamos a usar, en este caso:
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
Estos componentes van a servir para implementar el Drag&Drop:
-
DragDropContext le dará a nuestra aplicación la capacidad de usar la librería.
-
Droppable es un componente que le permite a la librería saber dónde se pueden soltar los elementos. Es como un contenedor que puede contener elementos que se pueden arrastrar. Podemos tener una o varias areas hello-pangea/dnd.
-
Draggable es un componente que le permite a la librería saber qué elementos se pueden arrastrar.
App.jsx después de la importación:
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function App() {
const initialTodos = [
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Aprender Js"},
{ id: 3, text: "Aprender Vue"},
];
const [todos, setTodos] = useState(initialTodos);
return (
<div>
<h1>Drag & Drop</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
export default App;
DragDropContext
Ahora ya podemos empezar a trabajar con la librería, vamos a utilizar el componente
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function App() {
const initialTodos = [
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Aprender Js"},
{ id: 3, text: "Aprender Vue"},
];
const [todos, setTodos] = useState(initialTodos);
return (
<DragDropContext>
<h1>Drag & Drop</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</DragDropContext>
);
}
export default App;
Droppable
Ahora necesitamos crear un área Droppable, es decir nos permitirá proporcionar un área específica donde nuestros items puedan ser movidos.
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function App() {
const initialTodos = [
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Aprender Js" },
{ id: 3, text: "Aprender Vue" },
];
const [todos, setTodos] = useState(initialTodos);
return (
<DragDropContext>
<h1>Drag & Drop</h1>
<Droppable droppableId="droppable-1">
{(droppableProvided) => (
<ul
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
</Droppable>
</DragDropContext>
);
}
export default App;
- droppableId: Es un identificador único que se usa para identificar esta instancia específica de Droppable.
- droppableProvider: Nos permite acceder a la información de estado de la librería.
- droppableProvider.innerRef: Esto creará una referencia (provided.innerRef) para que la librería acceda al elemento HTML del elemento de la lista.
- droppableProvider.droppableProps: referencia API
Draggable
Ahora usaremos el componente Draggable, que nuevamente, similar al componente Droppable, incluirá una función en la que pasaremos los accesorios a los componentes de nuestro elemento de lista.
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function App() {
const initialTodos = [
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Aprender Js" },
{ id: 3, text: "Aprender Vue" },
];
const [todos, setTodos] = useState(initialTodos);
return (
<DragDropContext>
<h1>Drag & Drop</h1>
<Droppable droppableId="droppable-1">
{(droppableProvided) => (
<ul
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
{todos.map((todo, index) => (
<Draggable key={todo.id} draggableId={`${todo.id}`} index={index}>
{(dragableProvided) => (
<li
ref={dragableProvided.innerRef}
{...dragableProvided.draggableProps}
{...dragableProvided.dragHandleProps}
>
{todo.text}
</li>
)}
</Draggable>
))}
</ul>
)}
</Droppable>
</DragDropContext>
);
}
export default App;
- draggableId={todo.id} : Identificador único que se usa para identificar esta instancia específica de Draggable.
- index={index} : Es el índice de la lista de elementos que se está iterando. Se usa para determinar el orden de los elementos en la lista.
- draggableProvider.innerRef: Esto creará una referencia ( provided.innerRef) para que la librería acceda al elemento HTML del elemento de la lista.
- draggableProvider.draggableProps y draggableProvider.dragHandleProps: referencia API
droppableProvider.placeholder
Se puede utilizar un espacio reservado para mostrar dónde se colocará el elemento cuando se suelta.
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function App() {
const initialTodos = [
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Aprender Js" },
{ id: 3, text: "Aprender Vue" },
];
const [todos, setTodos] = useState(initialTodos);
return (
<DragDropContext>
<h1>Drag & Drop</h1>
<Droppable droppableId="droppable-1">
{(droppableProvided) => (
<ul
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
{todos.map((todo, index) => (
<Draggable key={todo.id} draggableId={`${todo.id}`} index={index}>
{(dragableProvided) => (
<li
ref={dragableProvided.innerRef}
{...dragableProvided.draggableProps}
{...dragableProvided.dragHandleProps}
>
{todo.text}
</li>
)}
</Draggable>
))}
{droppableProvided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
);
}
export default App;
Reordenar los elementos despues del drag & drop
Para resolverlo, podemos ver que DragDropContext toma un props onDragEnd que nos permitirá activar una función después de que se haya completado el arrastre.
<DragDropContext onDragEnd={handleonDragEnd}>
Ahora implementamos la función handleonDragEnd y observamos que nos devuelve, veremos que nos da la posición iniicial y final del elemento movido, con eso podemos ya ordenar el array:
const handleonDragEnd = (result) => {
const { destination, source } = result;
if (!destination) return;
//console.log(result);
const startIndex = source.index;
const endIndex = destination.index;
reorder(startIndex, endIndex);
};
const reorder = (startIndex, endIndex) => {
const result = [...todos];
//const removed = result.splice(startIndex, 1);
const [removed] = result.splice(startIndex, 1);
//console.log(removed);
//console.log(result);
result.splice(endIndex, 0, removed);
//console.log(result);
setTodos(result);
};
- splice js: El método splice() cambia el contenido de un array eliminando elementos existentes y/o agregando nuevos elementos.
-
Sintaxis: array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
-
start: Indice donde se comenzará a cambiar el array.
- deleteCount: Número de elementos (enteros) a eliminar, comenzando por start.
- Si es 1, se eliminará un elemento.
- Si es 0, no se eliminarán elementos. En este caso, se debe especificarse al menos un nuevo elemento.
- item1, item2, ...: Los elementos que se agregarán al array. Si no se especifican, splice() solo eliminará elementos del array.
Código final
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function App() {
const initialTodos = [
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Aprender Js" },
{ id: 3, text: "Aprender Vue" },
];
const [todos, setTodos] = useState(initialTodos);
const handleonDragEnd = (result) => {
const { destination, source } = result;
if (!destination) return;
const startIndex = source.index;
const endIndex = destination.index;
reorder(startIndex, endIndex);
};
const reorder = (startIndex, endIndex) => {
const result = [...todos];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
setTodos(result);
};
return (
<DragDropContext onDragEnd={handleonDragEnd}>
<h1>Drag & Drop</h1>
<Droppable droppableId="droppable-1">
{(droppableProvided) => (
<ul
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
{todos.map((todo, index) => (
<Draggable key={todo.id} draggableId={`${todo.id}`} index={index}>
{(dragableProvided) => (
<li
ref={dragableProvided.innerRef}
{...dragableProvided.draggableProps}
{...dragableProvided.dragHandleProps}
>
{todo.text}
</li>
)}
</Draggable>
))}
{droppableProvided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
);
}
export default App;