
Hay muchas librerías para hacer drag and drop de elementos de forma más o menos sencilla
Aquí exploro la librería react-beautiful-dnd
, posiblemente una de las mejores
Prólogo
Para implementar el movimiento dran-n-drop en un elemento basta con definir qué zonas son contenedores y qué elementos son movibles
Pero eso se dice bastante más rápido de lo que se hace
La librería react-beautiful-dnd
, de Atlassian (los compradores de Trello, aquí tienes el artículo donde la presentaron) es una librería que nos facilita esta implementación orientada a una aplicación precisamente como la de Trello
Vale, pues vamos allá
Generando los elementos
La idea es tener dos columnas con elementos que se puedan mover entre ellas
Bien, pues necesitamos definir esos elementos
Lo hago en un entorno React
, si lo quieres probar te recomiendo codesandbox.io
import React from 'react'
import styled from 'styled-components'
export default () => (
<Grid>
<Ul>
<Li>Elemento 1</Li>
<Li>Elemento 2</Li>
<Li>Elemento 3</Li>
</Ul>
<Ul>
<Li>Elemento 4</Li>
<Li>Elemento 5</Li>
<Li>Elemento 6</Li>
</Ul>
</Grid>
)
const Grid = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 50px;
background: #f5f4f3;
width: 500px;
`
const Ul = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
`
const Li = styled.li`
background: #e2e2e2;
padding: 5px;
margin: 5px;
border-radius: 3px;
`
Importo la librería de React
y también el styled
de styled-components
, que me permitirá definir los estilos a la manera de css-in-js
Defino los componentes css
Y listos, ya sólo me queda la parte de JavaScript

Drag and Drop
Parece sencillo, pero no lo es tanto
Si definimos los elementos como hemos hecho arriba, tendremos problemas para reorganizarlos ya que deberíamos acceder al DOM moverlos, actualizarlo, etc etc
Mucho mejor si trabajamos con una estructura de datos, y luego representamos esa estructura en formato html
Bien, pues vamos a hacerlo, y lo hacemos con un useState
ya que queremos que esta estructura sea persistente y no se nos reinicialice cada vez que haya un repintado
import React, { useState } from 'react'
import styled from 'styled-components'
const myElements = [
{
columnId: 'menu-1',
items: [
{
id: 'elemento-1',
text: 'Elemento 1',
},
{
id: 'elemento-2',
text: 'Elemento 2',
},
{
id: 'elemento-3',
text: 'Elemento 3',
},
],
},
{
columnId: 'menu-2',
items: [
{
id: 'elemento-4',
text: 'Elemento 4',
},
{
id: 'elemento-5',
text: 'Elemento 5',
},
{
id: 'elemento-6',
text: 'Elemento 6',
},
],
},
]
export default () => {
const onDragEnd = () => {}
const [elements, setElements] = useState(myElements)
return (
<Grid>
{elements.map((col, i) => (
<Ul>
{col.items.map((el, j) => (
<Li>{el.text}</Li>
))}
</Ul>
))}
</Grid>
)
}
Perfecto, ahora ya sí que vamos bien
En cada elemento de los arrays de la estructura le defino también un id
aparte del text
, esto lo hago como requisito de la librería react-beautiful-dnd
Seguimos
Lo primero es definir toda el área donde pasará todo, y lo hacemos con <DragDropContext>
return (
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
{elements.map((col, i) => (
<Ul>
{col.items.map((el, j) => (
<Li>{el.text}</Li>
))}
</Ul>
))}
</Grid>
</DragDropContext>
)
Ahora necesito definir las zonas donde podamos soltar los elementos, lo hago con <Droppable>
, y serán las dos columnas
return (
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
{elements.map((col, i) => (
<Droppable droppableId={col.columnId}>
<Ul>
{col.items.map((el, i) => (
<Li>{el.text}</Li>
))}
</Ul>
</Droppable>
))}
</Grid>
</DragDropContext>
)
El atributo droppableId
tiene que ser único para cada unidad, utilizo el que ya he definido en la estructura de datos
Vamo bien
Pero lo de arriba no funciona
Por qué?
Porque <Droppable>
no es un componente "normal" sino que es un Render Prop
Un componente se define así
const MiComponente = ({ children }) => <div>{children}</div>
Y un Render Prop
se definen así
const MiComponente = ({ children }) => children(valor)
Cuando utilizamos un componente lo hacemos así
<MiComponente>qué tal</MiComponente>
Y cuando utilizamos un render prop
<MiComponente>{() => <>qué tal</>}</MiComponente>
La utilidad de los render props es que en la función recibimos información que nos es útil, y es el caso que nos ocupa
Dicho esto, lo reescribo para que funcione
return (
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
{elements.map((col, i) => (
<Droppable droppableId={col.columnId}>
{(provided, snapshot) => (
<Ul>
{col.items.map((el, j) => (
<Li>{el.text}</Li>
))}
</Ul>
)}
</Droppable>
))}
</Grid>
</DragDropContext>
)
Los valores que recibo son provided
y snapshot
, luego los utilizaré
Seguimos, ahora viene el definir los elementos que puedo mover, y los defino con <Draggable>
Y este Draggable
es, al igual que antes, un Render Prop
así que ya lo añado como toca
return (
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
{elements.map((col, i) => (
<Droppable droppableId={col.columnId} key={col.columnId}>
{(provided, snapshot) => (
<Ul>
{col.items.map((el, j) => (
<Draggable draggableId={el.id} index={j} key={el.id}>
{(provided, snapshot) => <Li>{el.text}</Li>}
</Draggable>
))}
</Ul>
)}
</Droppable>
))}
</Grid>
</DragDropContext>
)
Al igual que antes tenemos un atributo draggableId
y en este caso su valor también lo referimos al id
de la estructura de datos
También aprovecho y añado los key
para que React
no proteste
Y siguiendo las instrucciones de la librería también añadimos un index
para <Draggable>
que no puede contener el valor de la key
Y al igual de antes recibo provided
y snapshot
, luego los utilizaré
Ahora es el momento de utilizar los props
que recibimos con el Render Props
, siguiendo las directrices de la librería
return (
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
{elements.map((col, i) => (
<Droppable droppableId={col.columnId} key={col.columnId}>
{(provided, snapshot) => (
<Ul ref={provided.innerRef}>
{col.items.map((el, j) => (
<Draggable draggableId={el.id} index={j} key={el.id}>
{(provided, snapshot) => (
<Li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
{el.text}
</Li>
)}
</Draggable>
))}
{provided.placeholder}
</Ul>
)}
</Droppable>
))}
</Grid>
</DragDropContext>
)
Como ves se trata de referencias para asignar a los componentes, y de props
que volcamos también a los distintos componentes, más el provided.placeholder
, un componente que lo ponemos al final de todos los ítems que hemos volcado
Y ya lo tenemos
Aunque eso sí, nos falta definir la función onDragEnd
Reordenando la lista
Teníamos la función onDragEnd
que se ejecutaba cuando dejábamos un elemento que habíamos arrastrado previamente
Esa función recibe el argumento result
, y lo primero que haremos será retornar el valor (es decir, no hacer nada) si no hay destinación (se ha soltado el elemento fuera de la zona pertinente), o si la destinación es la misma que el origen (con lo que no tenemos que reordenar nada)
const onDragEnd = result => {
if (!result.destination) return
if (result.destination.index === result.source.index) return
}
Si no es ninguna de estos dos casos, lo que tenemos que hacer es reordenar la lista (la estructura de datos del principio)
Una vez esté reordenada, React ya nos repintará el render y veremos cómo la lista se reordena como le corresponde
Para reordenar la lista tenemos los índices que ya los hemos definido en la estructura con los id
Los nuevos índices nos vienen con result
y el índice de inicio result.source.index
y de destinación result.destination.index
Bien, pues ahora basta con quitar el elemento concreto e insertarlo a donde toca, lo hacemos con splice
, y encontramos los elementos que tocan con findIndex
Mejor si lo encapsulamos en una función aparte, y mejor si en lugar de cambiar la lista generamos una lista nueva
Si evitamos modificar variables (principio de inmutabilidad) nos ahorramos toda una serie de bugs típicos que ocurren cuando modificamos variables
Respetar ese principio es uno de los pilares de la programación funcional
// función que se ejecuta al soltar un elemento
const onDragEnd = result => {
// desestructuramos el argumento
const { destination, source, draggableId } = result
// si no aplica, devolvemos y abortamos
if (!destination) return
if (destination.droppableId === source.droppableId && destination.index === source.index) return
// pedimos una nueva lista ya ordenada
const new_elements = reorder(elements, source.droppableId, destination.droppableId, source.index, destination.index)
// actualizamos la lista en la variable de estado elements
setElements(new_elements)
}
// función para reordenar la lista
const reorder = (list, startColumn, endColumn, startIndex, endIndex) => {
// clonamos la lista
const newList = Array.from(list)
// encontramos la columna de inicio dentro de la lista
const startColumnIndex = newList.findIndex(item => item.columnId === startColumn)
// encontramos la columna final dentro de la lista
const endColumnIndex = newList.findIndex(item => item.columnId === endColumn)
// quitamos el elemento de la columna original
const [removed] = newList[startColumnIndex].items.splice(startIndex, 1)
// añadimos ese elemento dentro de la nueva posición
newList[endColumnIndex].items.splice(endIndex, 0, removed)
// y devolvemos
return newList
}
Y ya lo tenemos
Puedes ver el código completo aquí, donde le he cambiado el nombre del componente para que también puedas verlo en codesandbox
(recuerda que tienes que añadirle las librerías pertinentes)
import React, { useState } from 'react'
import styled from 'styled-components'
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'
const myElements = [
{
columnId: 'menu-1',
items: [
{
id: 'elemento-1',
text: 'Elemento 1',
},
{
id: 'elemento-2',
text: 'Elemento 2',
},
{
id: 'elemento-3',
text: 'Elemento 3',
},
],
},
{
columnId: 'menu-2',
items: [
{
id: 'elemento-4',
text: 'Elemento 4',
},
{
id: 'elemento-5',
text: 'Elemento 5',
},
{
id: 'elemento-6',
text: 'Elemento 6',
},
],
},
]
const reorder = (list, startColumn, endColumn, startIndex, endIndex) => {
const newList = Array.from(list)
const startColumnIndex = newList.findIndex(item => item.columnId === startColumn)
const endColumnIndex = newList.findIndex(item => item.columnId === endColumn)
const [removed] = newList[startColumnIndex].items.splice(startIndex, 1)
newList[endColumnIndex].items.splice(endIndex, 0, removed)
return newList
}
export default () => {
const [elements, setElements] = useState(myElements)
const onDragEnd = result => {
const { destination, source, draggableId } = result
if (!destination) return
if (destination.droppableId === source.droppableId && destination.index === source.index) return
const new_elements = reorder(elements, source.droppableId, destination.droppableId, source.index, destination.index)
setElements(new_elements)
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
{elements.map((col, i) => (
<Droppable droppableId={col.columnId} key={col.columnId}>
{(provided, snapshot) => (
<Ul ref={provided.innerRef}>
{col.items.map((el, j) => (
<Draggable draggableId={el.id} index={j} key={el.id}>
{(provided, snapshot) => (
<Li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
{el.text}
</Li>
)}
</Draggable>
))}
{provided.placeholder}
</Ul>
)}
</Droppable>
))}
</Grid>
</DragDropContext>
)
}
const Grid = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 50px;
background: #f5f4f3;
width: 500px;
`
const Ul = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
`
const Li = styled.li`
background: #e2e2e2;
padding: 5px;
margin: 5px;
border-radius: 3px;
`
🙋♂️
Lista de correo: escribo algo de tanto en cuanto