логотип PurpleSchool
логотип PurpleSchool

3 примера реализации drag-and-drop во Vue

Автор

Олег Марков

Введение

Drag-and-drop, или перетаскивание элементов, — важная часть многих современных веб-приложений. Возможность перетаскивать задачи между списками, сортировать товарные карточки или собирать корзину покупок делает пользовательский интерфейс интуитивно понятным и удобным. В этой статье я покажу вам три разных способа реализовать drag-and-drop во Vue: начнем с базового перетаскивания с помощью стандартных HTML5 API, затем рассмотрим сортировку сложного списка, а в третьем примере используем одну из популярных drag-and-drop библиотек для Vue.

Каждый пример мы разберем подробно, чтобы вы могли сразу повторить его у себя – с разбором ключевых моментов, особенностей и потенциальных проблем. Также я подготовил комментарии прямо в коде, чтобы вы не запутались, даже если только начинаете знакомиться с Vue.


Пример 1. Базовый drag-and-drop на чистом Vue без сторонних библиотек

Часто для drag-and-drop достаточно базовых возможностей браузера — это быстро и подходит для большинства задач, где не нужна сложная логика сортировки списка. Давайте разберем рабочий пример, где вы сможете перетаскивать элементы списка из одного контейнера в другой.

Исходные данные

Допустим, у нас есть два списка: "доступные элементы" и "выбранные элементы". Задача — перемещать элементы между этими списками с помощью drag-and-drop.

Вот шаблон компонента:

<template>
  <div class="dnd-container">
    <div class="list" 
         @dragover.prevent
         @drop="onDropList('available')">
      <h4>Доступные</h4>
      <div v-for="item in availableItems"
           :key="item.id"
           class="list-item"
           draggable="true"
           @dragstart="onDragStart(item, 'available')">
        {{ item.name }}
      </div>
    </div>
    <div class="list"
         @dragover.prevent
         @drop="onDropList('selected')">
      <h4>Выбранные</h4>
      <div v-for="item in selectedItems"
           :key="item.id"
           class="list-item"
           draggable="true"
           @dragstart="onDragStart(item, 'selected')">
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

JavaScript часть компонента:

<script>
export default {
  data() {
    return {
      availableItems: [
        { id: 1, name: 'Элемент 1' },
        { id: 2, name: 'Элемент 2' },
        { id: 3, name: 'Элемент 3' }
      ],
      selectedItems: [],
      dragItem: null, // Здесь храним перетаскиваемый элемент и его место
      dragList: ''
    }
  },
  methods: {
    onDragStart(item, listType) {
      // Запоминаем, что именно перетаскиваем
      this.dragItem = item
      this.dragList = listType
    },
    onDropList(targetList) {
      if (!this.dragItem) return
      // Определяем исходный и целевой списки
      let fromList, toList
      if (this.dragList === 'available' && targetList === 'selected') {
        fromList = this.availableItems
        toList   = this.selectedItems
      } else if (this.dragList === 'selected' && targetList === 'available') {
        fromList = this.selectedItems
        toList   = this.availableItems
      } else { 
        return // Нельзя перенести сам в себя
      }
      // Удаляем из исходного списка
      const index = fromList.findIndex(item => item.id === this.dragItem.id)
      if (index !== -1) {
        const [movedItem] = fromList.splice(index, 1)
        // Добавляем в целевой список
        toList.push(movedItem)
      }
      // Очищаем dragItem
      this.dragItem = null
    }
  }
}
</script>

CSS (для наглядности):

.dnd-container {
  display: flex;
  gap: 32px;
}
.list {
  width: 150px;
  border: 1px solid #ccc;
  min-height: 120px;
  padding: 10px;
  border-radius: 4px;
}
.list-item {
  padding: 8px;
  margin-bottom: 6px;
  background: #f1f1f1;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  cursor: grab;
}

Ключевые детали реализации

  • Атрибут draggable="true" позволяет делать любой элемент "перетаскиваемым".
  • Обработчик @dragstart нужен, чтобы Vue запомнил, что именно перетаскивается.
  • Событие @dragover.prevent на контейнере не даёт браузеру отклонять дроп.
  • Событие @drop переносит элемент из одного массива во второй.

Такой подход подойдет для простых интерфейсов без сортировки внутри списка.


Пример 2. Drag-and-drop сортировка элементов в списке

Теперь давайте усложним задачу: реализуем сортировку списка с помощью drag-and-drop. Пользователь должен иметь возможность менять порядок элементов прямо в списке.

Шаблон компонента

<template>
  <div class="sortable-list">
    <div v-for="(item, idx) in items"
         :key="item.id"
         class="sortable-item"
         draggable="true"
         @dragstart="onDragStart(idx)"
         @dragover.prevent="onDragOver(idx)"
         @drop="onDrop(idx)"
         :class="{ 'drag-over': idx === dragOverIndex }">
      {{ item.name }}
    </div>
  </div>
</template>

JavaScript

<script>
export default {
  data() {
    return {
      items: [
        { id: 101, name: 'Карточка A' },
        { id: 102, name: 'Карточка B' },
        { id: 103, name: 'Карточка C' }
      ],
      dragStartIndex: null, // Индекс элемента, который перетаскиваем
      dragOverIndex: null  // Индекс элемента, над которым находимся
    }
  },
  methods: {
    onDragStart(idx) {
      // Запоминаем, с какого индекса начали перетаскивание
      this.dragStartIndex = idx
    },
    onDragOver(idx) {
      // При наведении устанавливаем индекс поверх которого перетаскиваем
      this.dragOverIndex = idx
    },
    onDrop(idx) {
      if (this.dragStartIndex === null) return
      // Получаем копию массива
      const movedItem = this.items[this.dragStartIndex]
      this.items.splice(this.dragStartIndex, 1) // Удаляем элемент, который двигаем
      this.items.splice(idx, 0, movedItem) // Вставляем на новое место
      // Сбрасываем индексы
      this.dragStartIndex = null
      this.dragOverIndex = null
    }
  }
}
</script>

Немного стилей для видимости

.sortable-list {
  width: 280px;
  border: 1px solid #ccc;
  padding: 10px;
  border-radius: 7px;
  background: #fafafa;
}
.sortable-item {
  padding: 10px;
  margin-bottom: 8px;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: move;
  transition: background .24s;
}
.sortable-item.drag-over {
  background: #e0fcff;
}

В чём суть реализации

  • Для каждого элемента списка добавляем обработчики @dragstart, @dragover.prevent и @drop.
  • Перетаскиваемый элемент определяется по его индексу.
  • На событии drop удаляем перетаскиваемый элемент из текущей позиции и вставляем в новую.
  • Для визуализации наведения используем динамический класс drag-over.

Обратите внимание: у такого подхода есть ограничения по производительности на больших списках и по универсальности. Для сложных кейсов лучше использовать сторонние библиотеки.


Пример 3. Drag-and-drop через библиотеку vue-draggable-next

Когда необходимо реализовать drag-and-drop с расширенными возможностями (например, поддержка каскадной сортировки, drag handle, ограничение по областям и т.д.), стоит воспользоваться библиотекой.

Очень популярна для Vue 3 библиотека vue-draggable-next — это адаптация SortableJS под Vue.

Установка библиотеки

Сначала устанавливаем пакет:

npm install vuedraggable@next

Использование

Разберем кейс сортировки списка задач с drag-and-drop:

<template>
  <div>
    <Draggable 
      v-model="tasks"
      group="tasks"
      item-key="id"
      @end="onChangeOrder"
      ghost-class="dragged-placeholder"
    >
      <template #item="{element}">
        <div class="task-item">
          {{ element.title }}
        </div>
      </template>
    </Draggable>
  </div>
</template>

JS-часть

<script>
import Draggable from "vuedraggable"

export default {
  components: { Draggable },
  data() {
    return {
      tasks: [
        { id: 1, title: 'Написать документацию' },
        { id: 2, title: 'Встретиться с командой' },
        { id: 3, title: 'Запустить тесты' }
      ]
    }
  },
  methods: {
    onChangeOrder(evt) {
      // evt - объект события библиотеки, можно использовать для логирования
      // после изменения порядка tasks уже обновится
      console.log('Новый порядок:', this.tasks)
    }
  }
}
</script>

CSS-пример

.task-item {
  padding: 12px 16px;
  margin-bottom: 10px;
  background: #f7fdff;
  border: 1px solid #b2e4fa;
  border-radius: 4px;
  transition: box-shadow .2s;
}
.dragged-placeholder {
  background: #e2edfa !important;
  border-style: dashed !important;
}

Ключевые достоинства подхода

  • Простота — достаточно использовать компонент Draggable и v-model.
  • В библиотеке реализована обработка событий, например, onStart, onEnd, onAdd и т.д.
  • Работа со вложенными списками, drag handle, блокировка элементов, кастомизация поведения.
  • Можно использовать между несколькими контейнерами (drag между списками).

Расширенные возможности

Пример работы с переносом задач между двумя списками:

<template>
  <div class="kanban-columns">
    <Draggable v-model="todo"
               group="shared"
               item-key="id"
               class="kanban-col"
               :clone="cloneTask">
      <template #item="{element}">
        <div class="task">{{element.title}}</div>
      </template>
      <div slot="header">TODO</div>
    </Draggable>
    <Draggable v-model="done"
               group="shared"
               item-key="id"
               class="kanban-col"
               :clone="cloneTask">
      <template #item="{element}">
        <div class="task">{{element.title}}</div>
      </template>
      <div slot="header">DONE</div>
    </Draggable>
  </div>
</template>

<script>
import Draggable from 'vuedraggable'
export default {
  data() {
    return {
      todo: [
        {id: 1, title: 'Выучить Vue'},
        {id: 2, title: 'Поработать с draggable'}
      ],
      done: [
        {id: 3, title: 'Позавтракать'}
      ]
    }
  },
  components: {Draggable},
  methods: {
    cloneTask(task) {
      // Создает копию объекта задачи при переносе между колонками. 
      // В некоторых кейсах нужно, чтобы не мутировать исходник.
      return {...task}
    }
  }
}
</script>

Когда стоит использовать библиотеку?

  • Когда необходимы сложные сценарии drag-and-drop (например, вложенность, много колонок, высокий уровень детализации поведения).
  • Если нужны расширения вроде кастомизации placeholder, drag handle, предотвращения перемещений.
  • Для больших проектов с широкой аудиторией.

Заключение

Мы рассмотрели три подхода к реализации drag-and-drop во Vue: от простейшего способа на стандартных событиях HTML5, до сортировки со сменой порядка элементов и, наконец, использование специализированной библиотеки vue-draggable-next. Каждый из способов подходит для своих задач — выбор зависит от ваших требований к функциональности, объему кода и необходимости поддержки сложных кейсов.

Для минимальных интерфейсов подходит первый способ, второй — для сортировки внутри одного списка, а третий — когда нужен масштабируемый и расширяемый drag-and-drop функционал.


Частозадаваемые технические вопросы по теме и ответы

Как реализовать drag-and-drop между двумя разными компонентами во Vue?

Чтобы реализовать drag-and-drop между независимыми компонентами, нужно хранить состояние (например, перетаскиваемый элемент) на уровне родителя или глобального состояния (Vuex, Pinia), и триггерить изменения списков через props/events или store. В dragstart сообщайте родителю о начале перетаскивания, в drop — какой элемент и куда переместить.


Как предотвратить перенос одних и тех же элементов несколько раз?

Проверьте, нет ли повторного добавления элемента в массив при drop. В обработчике drop ищите элемент по id в целевом массиве — если найден, не добавляйте второй раз, либо, для уникальных коллекций, используйте Set.


Как добавить визуальную подсветку места вставки при сортировке drag-and-drop?

Добавьте динамический класс (например, drag-over) к элементу списка, над которым сейчас находится курсор (dragOverIndex в примере выше). Через CSS измените фон или бордер у этого элемента для наглядной подсветки.


Почему в некоторых браузерах drag-and-drop не работает на мобильных устройствах?

HTML5 drag-and-drop API работает только на десктопах. Для мобильных устройств используйте библиотеки, поддерживающие touch-события (например, vue-draggable-next, сортировка на основе SortableJS). Альтернативно реализуйте touchmove/touchend обработчики для мобильных drag-and-drop.


Как реализовать drag handle (зона для перетаскивания) во Vue?

В компонентах вроде draggable можно задать параметр handle у текущего элемента, указывая селектор — только эта часть элемента будет реагировать на захват. Если пишете drag-and-drop сами — навесьте draggable и dragstart только на нужный вложенный элемент (кнопку/иконку).


Стрелочка влево5 примеров использования filter во Vue для упрощения разработки

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Управление переменными и реактивными свойствами во VueИспользование v for и slot в VueПрименение v-bind для динамической привязки атрибутов в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueИспользование transition во VueТестирование компонентов и приложений на VueРабота с teleport для управления DOM во VueПять шагов по настройке SSR в VuejsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовИспользование router-link для навигации в Vue RouterКак использовать require в Vue для динамического импорта модулейРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиРуководство по nextTick для работы с DOMСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний