React hello-pangea/dnd
React hello-pangea/dnd es una biblioteca que nos permite utilizar drag and drop interfaces en React. Esta diseñada para ser liviana, rápida y fácil de usar.
Recursos
- hello-pangea/dnd
- documentación api
- freecodecamp tutorial
- forwarding-refs
- Tutorial: Yoelvis Mulen { code }
Install
# npm
npm install @hello-pangea/dnd --save
# pnpm
pnpm add @hello-pangea/dnd
# yarn
yarn add @hello-pangea/dnd
Usage
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
DragDropContext le dará a nuestra aplicación la capacidad de usar la biblioteca. Funciona de manera similar a la API de contexto de React, donde la biblioteca ahora puede tener acceso al árbol de componentes.
Droppable es un componente que le permite a la biblioteca saber dónde se pueden soltar los elementos. Es como un contenedor que puede contener elementos que se pueden arrastrar.
Draggable es un componente que le permite a la biblioteca saber qué elementos se pueden arrastrar.
Droppable
Queremos crear un área Droppable, lo que significa que esto nos permitirá proporcionar un área específica donde nuestros artículos se pueden mover dentro.
import { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
const initialTodos = [
{ id: 1, text: "Learn React", completed: true },
{ id: 2, text: "Learn Firebase", completed: true },
{ id: 3, text: "Learn GraphQL", completed: false },
];
const App = () => {
const [todos, setTodos] = useState(initialTodos);
return (
<Droppable droppableId="todos">
<h1>Todo App</h1>
{(droppableProvider) => (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
</Droppable>
);
};
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 biblioteca.
- droppableProvider.innerRef: Esto creará una referencia ( provided.innerRef) para que la biblioteca acceda al elemento HTML del elemento de la lista.
- droppableProvider.droppableProps: referencia API
<DragDropContext>
<h1>Todo App</h1>
<Droppable droppableId="todos">
{(droppableProvider) => (
<ul
ref={droppableProvider.innerRef}
{...droppableProvider.droppableProps}
>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
</Droppable>
</DragDropContext>
li {
border: 2px solid salmon;
padding: 0.5rem;
}
ul {
list-style: none;
padding: 1rem;
border: 2px solid chocolate;
}
Draggable
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.
- key=
- draggableId={todo.id} : Es un 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. Esto se usa para determinar el orden de los elementos en la lista.
{
todos.map((todo, index) => (
<Draggable key={todo.id} index={index} draggableId={todo.id}>
{(draggableProvider) => <li key={todo.id}>{todo.text}</li>}
</Draggable>
));
}
- draggableProvider.innerRef: Esto creará una referencia ( provided.innerRef) para que la biblioteca acceda al elemento HTML del elemento de la lista.
- draggableProvider.draggableProps y draggableProvider.dragHandleProps: referencia API
{
todos.map((todo, index) => (
<Draggable key={todo.id} index={index} draggableId={todo.id}>
{(draggableProvider) => (
<li
ref={draggableProvider.innerRef}
{...draggableProvider.draggableProps}
{...draggableProvider.dragHandleProps}
>
{todo.text}
</li>
)}
</Draggable>
));
}
draggableId
Invariant failed: Draggable requires a [string] draggableId.
draggableId={
${todo.id}}
droppableProvider.placeholder
Se puede utilizar un espacio reservado para mostrar dónde se colocará el elemento cuando se suelta.
<DragDropContext>
<h1>Todo App</h1>
<Droppable droppableId="todos">
{(droppableProvider) => (
<ul
ref={droppableProvider.innerRef}
{...droppableProvider.droppableProps}
>
{todos.map((todo, index) => (
<Draggable
key={todo.id}
index={index}
draggableId={`${todo.id}`}
>
{(draggableProvider) => (
<li
ref={draggableProvider.innerRef}
{...draggableProvider.draggableProps}
{...draggableProvider.dragHandleProps}
>
{todo.text}
</li>
)}
</Draggable>
))}
{droppableProvider.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
Persistir el orden de los elementos
Para resolver esto, DragDropContext toma un props onDragEnd que nos permitirá activar una función después de que se haya completado el arrastre.
- splice js: El método splice() cambia el contenido de un array eliminando elementos existentes y/o agregando nuevos elementos.
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, 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.
const handleDragEnd = (result) => {
// console.log(result);
if (!result.destination) return;
console.log("origen: ", result.source.index);
console.log("fin: ", result.destination.index);
const startIndex = result.source.index;
const endIndex = result.destination.index;
const items = [...todos];
// con splice estamos eliminando un elemento del array y devolviendo ese elemento
const [reorderedItem] = items.splice(startIndex, 1);
// con splice estamos insertando un elemento en el array
items.splice(endIndex, 0, reorderedItem);
setTodos(items);
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
...
Optimizado:
//https://github.com/ymulenll/react-beautiful-dnd-demo/blob/master/src/App.js
const reorder = (list, startIndex, endIndex) => {
const result = [...list];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const App = () => {
const [todos, setTodos] = useState(initialTodos);
const handleDragEnd = (result) => {
const { destination, source } = result;
if (!destination) return;
if (
source.index === destination.index &&
source.droppableId === destination.droppableId
)
return;
setTodos((prevTasks) =>
reorder(prevTasks, source.index, destination.index)
);
};
TODO app FrontenMentor
TodoList.jsx
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
import TodoItem from "./TodoItem";
const TodoList = ({ todos, removeTodo, updateTodo, handleDragEnd }) => {
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="todos">
{(droppableProvided) => (
<div
className="mt-8 overflow-hidden rounded-t-md bg-white transition-all duration-1000 dark:bg-gray-800 [&>article]:p-4"
{...droppableProvided.droppableProps}
ref={droppableProvided.innerRef}
>
{todos.map((todo, index) => (
<Draggable
key={todo.id}
draggableId={`${todo.id}`}
index={index}
>
{(provided) => (
<TodoItem
{...provided.draggableProps}
ref={provided.innerRef}
{...provided.dragHandleProps}
todo={todo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
)}
</Draggable>
))}
{droppableProvided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
export default TodoList;
TodoItem.jsx (no olvidar: forwardRef)
import IconCross from "./icons/IconCross";
import IconCheck from "./icons/IconCheck";
import React from "react";
const TodoItem = React.forwardRef(
({ todo, removeTodo, updateTodo, ...props }, ref) => {
const { id, title, completed } = todo;
return (
<article
className="flex gap-4 border-b border-b-gray-400 "
ref={ref}
{...props}
>
<button
className={`h-5 w-5 flex-none rounded-full border-2 ${
completed
? "grid place-items-center bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"
: "inline-block"
}`}
onClick={() => updateTodo(id)}
>
{completed && <IconCheck />}
</button>
<p
className={`grow text-gray-600 transition-all duration-1000 dark:text-gray-400 ${
completed && "line-through"
}`}
>
{title}
</p>
<button className="flex-none" onClick={() => removeTodo(id)}>
<IconCross />
</button>
</article>
);
}
);
export default TodoItem;
App.jsx
import { useEffect, useState } from "react";
import Header from "./components/Header";
import TodoComputed from "./components/TodoComputed";
import TodoCreate from "./components/TodoCreate";
import TodoFilter from "./components/TodoFilter";
import TodoList from "./components/TodoList";
const initialStateTodos = JSON.parse(localStorage.getItem("todos")) || [];
//https://github.com/ymulenll/react-beautiful-dnd-demo/blob/master/src/App.js
const reorder = (list, startIndex, endIndex) => {
const result = [...list];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const App = () => {
const [todos, setTodos] = useState(initialStateTodos);
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
const handleDragEnd = (result) => {
const { destination, source } = result;
if (!destination) return;
if (
source.index === destination.index &&
source.droppableId === destination.droppableId
)
return;
setTodos((prevTasks) =>
reorder(prevTasks, source.index, destination.index)
);
};
const createTodo = (title) => {
const newTodo = {
id: `${Date.now()}`,
title: title.trim(),
completed: false,
};
setTodos([...todos, newTodo]);
};
const removeTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const updateTodo = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const computedItemsLeft = todos.filter((todo) => !todo.completed).length;
const clearCompleted = () => {
setTodos(todos.filter((todo) => !todo.completed));
};
const [filter, setFilter] = useState("all");
const changeFilter = (filter) => setFilter(filter);
const filteredTodos = () => {
switch (filter) {
case "all":
return todos;
case "active":
return todos.filter((todo) => !todo.completed);
case "completed":
return todos.filter((todo) => todo.completed);
default:
return todos;
}
};
return (
<div className="min-h-screen bg-gray-300 bg-mobile-light bg-contain bg-no-repeat transition-all duration-1000 dark:bg-gray-900 dark:bg-mobile-dark md:bg-desktop-light md:dark:bg-desktop-dark">
<Header />
<main className="container mx-auto mt-8 px-4 md:max-w-xl">
<TodoCreate createTodo={createTodo} />
{todos.length > 0 ? (
<TodoList
todos={filteredTodos()}
removeTodo={removeTodo}
updateTodo={updateTodo}
handleDragEnd={handleDragEnd}
/>
) : (
<p>Cargando...</p>
)}
<TodoComputed
computedItemsLeft={computedItemsLeft}
clearCompleted={clearCompleted}
/>
<TodoFilter changeFilter={changeFilter} filter={filter} />
</main>
<footer className="mt-8 text-center dark:text-gray-400">
Drag and drop to reorder list
</footer>
</div>
);
};
export default App;