Documentación/Guías
Listas que comparten ítems
Las listas de un mismo grupo mueven, copian e intercambian ítems — toda la columna acepta drops, pull y put deciden qué entra y qué sale, las paletas copian y las listas controladas aplican cada drop a un store con historial para deshacer.
Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés
Cargando la demo…
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = reactive([
{
id: 'backlog',
title: 'Backlog',
stories: [
{ id: 's1', title: 'Saved searches', points: 5 },
{ id: 's2', title: 'Crash on empty board', points: 2, bug: true },
{ id: 's3', title: 'CSV export', points: 3 },
{ id: 's4', title: 'Dark mode email', points: 2 },
],
},
{
id: 'sprint',
title: 'Sprint 14',
stories: [{ id: 's5', title: 'Onboarding tour', points: 8 }],
},
]);
const wholeColumn = ref(true);
</script>
<template>
<DnDProvider>
<div class="planning">
<label class="switch">
<input v-model="wholeColumn" type="checkbox" />
<code>dropzone</code>: the whole column takes drops
</label>
<div class="columns">
<Column v-for="column in columns" :key="column.id" :column="column" :whole-column="wholeColumn" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.planning {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.switch {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
}
.switch input {
accent-color: #42b883;
}
.switch code {
font: 12px/1 ui-monospace, Menlo, Consolas, monospace;
color: #42b883;
}
.columns {
display: flex;
justify-content: center;
gap: 14px;
width: 100%;
}
</style>Un grupo
Las listas con el mismo nombre de group intercambian ítems. Por defecto, cada lista es un grupo
por sí sola.
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import StoryCard from './StoryCard.vue';
type Story = { id: string; title: string; points: number };
const props = defineProps<{ column: { id: string; title: string; stories: Story[] } }>();
const list = useTemplateRef<HTMLElement>('list');
const { items, isOver } = makeSortableList(list, {
id: props.column.id, // names the list in drop events
items: toRef(props.column, 'stories'), // a writable ref into the reactive column
group: 'planning', // same name: stories move between these lists
});
</script>
<template>
<section :class="{ over: isOver }">
<h3>{{ column.title }}</h3>
<ul ref="list">
<StoryCard v-for="story in items" :key="story.id" :story="story" />
</ul>
</section>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';
const columns = reactive([
{ id: 'backlog', title: 'Backlog', stories: [{ id: 's1', title: 'Saved searches', points: 5 }] },
{ id: 'sprint', title: 'Sprint 14', stories: [] },
]);
</script>
<template>
<Column v-for="column in columns" :key="column.id" :column="column" />
</template>
- Aquí cada columna es un componente, como suelen serlo las columnas. Un componente también puede crear varias listas, un Todo y un Done fijos uno al lado del otro: cada tarjeta se une a la lista cuyo elemento la contiene.
toRef(props.column, 'stories')es modificable porque el padre pasa un objeto reactivo. El drop escribe ambos arrays, el que dejó la historia y aquel en el que entró, y los puntos de las dos columnas se actualizan.idaparece en los eventos de drop comofrom.listyto.list, y es la clave dee.result, los nuevos arrays de cada lista que cambia el drop.idygroupse leen una sola vez.- Las claves deben ser únicas en todo el grupo.
El onDrop de una lista se ejecuta para los drops dentro de ella. Mover de backlog a
sprint llama al onDrop de la lista sprint, y su e.result contiene los nuevos arrays de
ambas listas.
Toda la columna acepta drops: dropzone
Una lista corta en una columna alta deja espacio vacío debajo de la última tarjeta. Con
dropzone, otro elemento acepta los drops en nombre de la lista: aquí, toda la columna. Una
historia soltada debajo de la última aterriza al final. Las historias se quedan en el <ul>;
solo crece la zona que acepta drops. La demo resalta esa zona mientras arrastras: desactívala y
solo la propia lista acepta la historia.
const zone = useTemplateRef<HTMLElement>('zone');
const { items } = makeSortableList(list, {
items: toRef(props.column, 'stories'),
group: 'planning',
dropzone: zone, // or a getter: () => (wholeColumn.value ? zone.value : undefined)
});
dropzone, una lista vacía sigue necesitando un tamaño para aceptar drops: dale un
min-height.Qué entra y qué sale: pull y put
group también acepta un objeto. pull indica qué puede salir de una lista, put qué puede
entrar:
Cargando la demo…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type SortablePutContext } from '@vue-dnd-kit/core';
import RuleList from './RuleList.vue';
// Four lists of one group, `files`. What each gives and takes is its `group.pull` and `group.put`.
const shortlist = reactive({
id: 'shortlist',
title: 'Shortlist',
code: 'put: ({ keys }) => shortlist.files.length + keys.length <= 3',
refusal: 'full',
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
files: [
{ id: 'pitch', name: 'pitch.pdf', kind: 'doc' as const },
{ id: 'mock', name: 'mockup.png', kind: 'image' as const },
],
});
const lists = reactive([
{
id: 'inbox',
title: 'Inbox',
code: '{ pull: true, put: false }',
refusal: 'put: false',
group: { name: 'files', pull: true, put: false },
files: [
{ id: 'logo', name: 'logo.svg', kind: 'image' as const },
{ id: 'brief', name: 'brief.pdf', kind: 'doc' as const },
{ id: 'hero', name: 'hero.png', kind: 'image' as const },
],
},
{
id: 'templates',
title: 'Templates',
code: "{ pull: 'clone', put: false }, sort: false",
refusal: 'put: false',
group: { name: 'files', pull: 'clone' as const, put: false },
sort: false,
files: [
{ id: 'invoice', name: 'invoice.doc', kind: 'doc' as const },
{ id: 'banner', name: 'banner.png', kind: 'image' as const },
],
},
{
id: 'images',
title: 'Images',
code: "put: 'image'",
refusal: 'images only',
group: { name: 'files', put: 'image' },
files: [],
},
shortlist,
]);
</script>
<template>
<DnDProvider>
<div class="rules">
<RuleList v-for="list in lists" :key="list.id" :list="list" />
</div>
</DnDProvider>
</template>
<style scoped>
.rules {
display: grid;
grid-template-columns: repeat(2, minmax(0, 260px));
gap: 12px;
justify-content: center;
}
@media (max-width: 480px) {
.rules {
grid-template-columns: minmax(0, 1fr);
}
}
</style>| Opción | Valores |
|---|---|
pull | true (por defecto): los ítems pueden salir. false: no pueden. 'clone': salen copias y los ítems se quedan. |
put | true (por defecto): los ítems pueden entrar. false: no entra nada. Un tipo o varios ('image', ['image', 'video']): entran los ítems de esos tipos. Una función: decides en cada arrastre. |
// Inbox: gives, takes nothing.
group: { name: 'files', pull: true, put: false }
// Templates: gives copies, takes nothing, keeps its order.
group: { name: 'files', pull: 'clone', put: false }, sort: false
// Images: takes images only.
group: { name: 'files', put: 'image' }
Los tipos son las opciones type de los ítems arrastrados (makeSortableItem(el, { item, type }));
los ítems sin tipo cuentan como el nombre del grupo. Con tipos en put, cada ítem arrastrado
debe tener uno de ellos (con acceptMode: 'some', entran los que lo tienen).
Una función put recibe { keys, items, types, from, to }: las claves y los ítems arrastrados,
sus tipos, la lista de la que vienen (null para los elementos arrastrables simples) y esta
lista. Los movimientos dentro de una lista siempre están permitidos: Inbox sigue ordenando sus
propios archivos.
Sobre una lista que rechaza el arrastre, el hueco vuelve a donde empezó el arrastre, así que las
listas muestran lo que haría un drop: nada. Un drop ahí se cancela y el ítem vuelve a su sitio.
El isAccepting de la lista es false durante un arrastre así, y isOver sigue siendo true
sobre ella: así sabe cada lista de la demo qué decir.
El hueco también vuelve a casa sobre un destino de drop propio fuera de las listas, como una
papelera: el drop ahí es del destino, y las listas conservan el ítem a menos que su onDrop lo
saque (event.source.remove()). Los destinos dentro de las listas (un drop dentro de un ítem) y
alrededor de ellas (un panel que contiene las listas) dejan el hueco donde está.
const { items, isOver, isAccepting } = makeSortableList(el, { … });
const { active } = useDnD();
const verdict = computed(() => (active.value ? (isAccepting.value ? 'takes' : 'refuses') : null));
Límites
Una lista que guarda tres archivos como máximo, la Shortlist:
const shortlist = reactive({
files: [/* … */],
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
});
Lee sus propios ítems cuando un arrastre pregunta. Llénala y rechazará el siguiente archivo, con el motivo en su cabecera.
Cuando se arrastran varios ítems seleccionados de distintos tipos, acceptMode indica si deben
pasar put 'all' (por defecto) o 'some'. Con 'some', los que pasan aterrizan y los demás
vuelven a su sitio. Consulta multi-drag.
Un grupo por lista
Una lista pertenece a un solo grupo: group recibe un nombre, nunca varios. Las listas de un
grupo comparten cada arrastre (sus borradores, lo que se arrastra, cuáles de ellas lo aceptan),
así que los ítems nunca pasan de un grupo a otro. Cuando los ítems deben ir solo en algunas
direcciones, pon las listas en un grupo y deja que put decida por from, la lista de la que
viene el arrastre:
// Review takes tasks from both teams. A team takes tasks back from Review,
// never from the other team.
const fromReview = ({ from }: SortablePutContext) => from === 'review';
makeSortableList(review, { id: 'review', items: reviewTasks, group: 'tasks' });
makeSortableList(frontend, { id: 'frontend', items: frontendTasks, group: { name: 'tasks', put: fromReview } });
makeSortableList(backend, { id: 'backend', items: backendTasks, group: { name: 'tasks', put: fromReview } });
put: ['a', 'b'] nombra los grupos de los que una lista toma
ítems. Aquí un array en put enumera los tipos de los ítems que una lista acepta. Para
tomar ítems de otras listas, ponlas en un grupo y decide por from, como arriba.Copiar: paletas y la tecla Alt
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Palette from './Palette.vue';
import Sheet from './Sheet.vue';
</script>
<template>
<!-- The palette and the page are lists of one group: copies go from one to the other. -->
<DnDProvider>
<div class="builder">
<Palette />
<Sheet />
</div>
</DnDProvider>
</template>
<style scoped>
.builder {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 24px;
}
</style>makeSortableList(list, {
items: templates,
group: { name: 'blocks', pull: 'clone', put: false }, // copies go out, nothing comes in
sort: false, // its own order stays
clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});
makeSortableList(list, {
items: blocks,
group: 'blocks',
clone: (block) => ({ ...block, id: crypto.randomUUID() }), // for Alt-drags
dropOutside: 'remove', // drag off the Page panel to delete
});
pull: 'clone'copia cada arrastre que sale de la lista. El original se queda, y la copia viaja y aterriza.- Mantener Alt mientras arrastras también copia, desde cualquier lista con una
función
clone: es la que hace la copia. Cambia la tecla con elcopyModifierdel provider, o desactívala connull. - En ambos casos, la lista de la que sale el ítem necesita
clone. Debe devolver la copia con una clave nueva. Sinclone, no se copia nada. - El evento de drop tiene
mode: 'copy', ye.itemsson las copias. Un componente de overlay también recibemode, para mostrar un «+» al copiar (consulta el overlay).
Intercambiar entre listas
Dos listas de un mismo grupo que tienen ambas behavior: 'swap' intercambian ítems entre sí: el
ítem arrastrado ocupa el sitio del que tiene debajo, y este pasa a la lista del arrastrado. Debe
poder salir de su lista (pull: true) y entrar en la otra (put). La rotación de guardias de
listas ordenables intercambia entre sus dos
columnas.
Elementos arrastrables simples
Una lista acoge ítems arrastrados con makeDraggable (un chip, una plantilla, un archivo de un
árbol) cuando tiene createItem. group.put también decide sobre ellos, con from: null.
Consulta elementos arrastrables simples en una lista.
Soltar fuera
Lo que pasa cuando un arrastre termina sin ningún destino debajo lo decide el dropOutside de la
lista:
dropOutside | Los ítems |
|---|---|
'placeholder' (por defecto) | Aterrizan donde se mostró el hueco por última vez. |
'cancel' | Vuelven a su sitio. Es el valor por defecto con placeholder: 'indicator'. |
'remove' | Salen de su lista: arrastrar fuera borra. La página de la demo de la paleta lo hace. |
'remove' pregunta primero al onBeforeDrop de la lista. Después se ejecuta el onDrop de la
lista con to.index === -1, y e.result contiene la lista sin los ítems. Una papelera que acepta
drops funciona igual: consulta zonas de papelera.
Listas controladas
Las listas pueden leer un store que no es suyo: Pinia, estado del servidor, datos inmutables.
Pasa items como getter (o un computed, o una ref de solo lectura): la biblioteca nunca escribe
en él. Llama a e.preventDefault() en onDrop y aplica e.result tú mismo:
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Table from './Table.vue';
import { createSeating } from './store';
const seating = createSeating();
const { state, undo } = seating;
</script>
<template>
<DnDProvider>
<div class="seating">
<div class="tables">
<Table id="a" title="Table A" :seating="seating" />
<Table id="b" title="Table B" :seating="seating" />
</div>
<section class="history">
<header>
<h4>History</h4>
<button type="button" :disabled="!state.history.length" @click="undo">Undo</button>
</header>
<ol>
<li v-for="(entry, index) in state.history" :key="state.history.length - index">{{ entry.label }}</li>
<li v-if="!state.history.length" class="none">Move a guest</li>
</ol>
</section>
</div>
</DnDProvider>
</template>
<style scoped>
.seating {
display: flex;
flex-direction: column;
gap: 12px;
width: 460px;
max-width: 100%;
margin: 0 auto;
}
.tables {
display: flex;
gap: 12px;
}
.history {
padding: 10px 12px;
border-radius: 12px;
background: color-mix(in srgb, currentColor 5%, transparent);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
h4 {
margin: 0;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
button {
padding: 5px 12px;
border: 0;
border-radius: 7px;
background: #42b883;
color: #fff;
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
button:disabled {
background: color-mix(in srgb, currentColor 12%, transparent);
color: inherit;
opacity: 0.5;
cursor: default;
}
ol {
display: flex;
flex-direction: column;
gap: 4px;
height: 66px;
margin: 8px 0 0;
padding: 0;
overflow-y: auto;
list-style: none;
font: 12px/1.4 ui-monospace, Menlo, Consolas, monospace;
}
li:first-child {
color: #42b883;
}
li.none {
color: inherit;
opacity: 0.5;
}
</style>const { items } = makeSortableList(list, {
id: props.id,
items: () => props.seating.state.tables[props.id]!, // read-only
group: 'seating',
onDrop: (e) => {
e.preventDefault(); // no write
props.seating.apply(e.result, `${e.items[0]?.name} → ${props.title}`);
},
});
const apply = (result: ReadonlyMap<Key, Guest[]>, label: string) => {
const before = [...result.keys()].map((id): [Key, Guest[]] => [id, state.tables[id]!]);
for (const [id, guests] of result) state.tables[id] = guests;
state.history.unshift({ label, before });
};
const undo = () => {
const last = state.history.shift();
for (const [id, guests] of last?.before ?? []) state.tables[id] = guests;
};
e.resultasocia cada lista que cambia el drop con su nuevo array: las dos mesas tras un movimiento, una tras una reordenación. Son arrays nuevos, así que el store puede guardar los antiguos para deshacer.- El borrador sigue al store: aplica
resultenonDrop(o antes de que se resuelva unonBeforeDropasíncrono) y el overlay aterriza en el nuevo orden. preventDefault()omite la escritura en todas las listas deresult. Si no lo aplicas en ningún sitio, las listas se quedan como estaban y los ítems vuelven a su sitio.- Las builds de desarrollo avisan cuando el drop de una lista de solo lectura se deja a la biblioteca: no cambiaría nada.
Para guardar en un servidor antes de que el drop aterrice, o para preguntar antes al usuario, consulta decidir un drop. Todas las opciones de las listas están en la API de makeSortableList.