Skip to content

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 ​

Install ​

sh
# npm
npm install @hello-pangea/dnd --save

# pnpm
pnpm add @hello-pangea/dnd

# yarn
yarn add @hello-pangea/dnd

Usage ​

jsx
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.

jsx
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
jsx
<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>
css
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.
jsx
{
    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
jsx
{
    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.

jsx
<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.
jsx
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:

jsx
//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

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)

jsx
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

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;