How Vue-Element-Admin Implements Drag-and-Drop Table Functionality Using vuedraggable

Vue-Element-Admin implements drag-and-drop tables by wrapping the vuedraggable component around list data, binding arrays to the :list prop, enabling cross-list dragging with the group attribute, and synchronizing state through custom helper methods.

The PanJiaChen/vue-element-admin repository provides a production-ready implementation of drag-and-drop table functionality using vuedraggable in the DndList component. This pattern allows users to reorder items within a single list or move items between two separate lists while maintaining reactive state synchronization.

Core Implementation Pattern in Vue-Element-Admin

The implementation follows a structured component-based architecture that leverages vuedraggable (version 2.20.0 as specified in package.json) to handle the underlying Sortable.js logic.

Importing and Registering vuedraggable

The component begins by importing the draggable library and registering it locally. This encapsulation ensures the drag-and-drop functionality remains modular and reusable across the application.

import draggable from 'vuedraggable'

export default {
  name: 'DndList',
  components: { draggable },
  props: {
    list1: {
      type: Array,
      default: () => []
    },
    list2: {
      type: Array,
      default: () => []
    }
  }
}

Binding Data with the :list Prop

The vuedraggable component requires a reactive array bound to the :list prop. In src/components/DndList/index.vue, two separate draggable instances manage list1 and list2 respectively. This binding ensures that any reordering or movement automatically mutates the source arrays.

<draggable :list="list1" group="article" class="dragArea">
  <!-- list1 items -->
</draggable>

<draggable :list="list2" group="article" class="dragArea">
  <!-- list2 items -->
</draggable>

Enabling Cross-List Dragging with group

To allow items to move between the two lists, both draggable instances share the same group name (article). This configuration creates a shared drag context where elements can transition from list1 to list2 and vice versa while maintaining the reactive data flow.

Handling Firefox Compatibility with setData

The implementation includes a workaround for a known Firefox bug where the drag operation fails without data in the dataTransfer object. The setData method injects empty text data to satisfy the browser requirement.

methods: {
  setData(dataTransfer) {
    dataTransfer.setData('Text', '')
  }
}

Custom Helper Methods for State Synchronization

Beyond basic dragging, the DndList component provides explicit control methods for programmatically moving items between lists. These helpers ensure data consistency when users prefer click-based interactions over drag gestures.

The deleteEle method removes an item from the left list (list1) and conditionally prepends it to the right list (list2) if not already present:

deleteEle(ele) {
  const idx = this.list1.findIndex(item => item.id === ele.id)
  if (idx !== -1) {
    this.list1.splice(idx, 1)
  }
  if (!this.list2.some(item => item.id === ele.id)) {
    this.list2.unshift(ele)
  }
}

Conversely, the pushEle method moves items from the right list back to the left:

pushEle(ele) {
  const idx = this.list2.findIndex(item => item.id === ele.id)
  if (idx !== -1) {
    this.list2.splice(idx, 1)
  }
  if (!this.list1.some(item => item.id === ele.id)) {
    this.list1.push(ele)
  }
}

Styling Drag States and Visual Feedback

The component uses scoped SCSS to provide visual cues during drag operations. It leverages Sortable.js's automatic class injection to style elements being dragged or targeted as drop zones.

.sortable-chosen {
  background: #4ab7bd !important;
  color: #fff;
}

.sortable-ghost {
  opacity: 0.5;
  background: #c8ebfb;
}

The .sortable-chosen class applies to the actively dragged element, while .sortable-ghost styles the placeholder indicating where the item will drop.

Complete Code Example

The following implementation demonstrates the full pattern used in src/components/DndList/index.vue:

<template>
  <div class="dndList">
    <div class="dndList-list">
      <h3>Draggable List 1</h3>
      <draggable 
        :list="list1" 
        group="article" 
        class="dragArea"
        :set-data="setData"
      >
        <div 
          v-for="element in list1" 
          :key="element.id" 
          class="list-complete-item"
        >
          <div class="list-complete-item-handle">
            {{ element.id }} {{ element.title }}
          </div>
          <div style="position:absolute;right:0px;">
            <span 
              style="float: right;margin-top: -20px;margin-right:5px;" 
              @click="deleteEle(element)"
            >
              <i style="color:#ff4949" class="el-icon-delete"/>
            </span>
          </div>
        </div>
      </draggable>
    </div>

    <div class="dndList-list">
      <h3>Draggable List 2</h3>
      <draggable 
        :list="list2" 
        group="article" 
        class="dragArea"
        :set-data="setData"
      >
        <div 
          v-for="element in list2" 
          :key="element.id" 
          class="list-complete-item"
        >
          <div 
            class="list-complete-item-handle2" 
            @click="pushEle(element)"
          >
            {{ element.id }} {{ element.title }}
          </div>
        </div>
      </draggable>
    </div>
  </div>
</template>

<script>
import draggable from 'vuedraggable'

export default {
  name: 'DndList',
  components: { draggable },
  props: {
    list1: {
      type: Array,
      default: () => []
    },
    list2: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    deleteEle(ele) {
      const idx = this.list1.findIndex(item => item.id === ele.id)
      if (idx !== -1) {
        this.list1.splice(idx, 1)
      }
      if (!this.list2.some(item => item.id === ele.id)) {
        this.list2.unshift(ele)
      }
    },
    pushEle(ele) {
      const idx = this.list2.findIndex(item => item.id === ele.id)
      if (idx !== -1) {
        this.list2.splice(idx, 1)
      }
      if (!this.list1.some(item => item.id === ele.id)) {
        this.list1.push(ele)
      }
    },
    setData(dataTransfer) {
      dataTransfer.setData('Text', '')
    }
  }
}
</script>

Summary

  • Vue-Element-Admin implements drag-and-drop table functionality using vuedraggable by wrapping the <draggable> component around list data in src/components/DndList/index.vue.
  • The :list prop creates two-way binding with reactive arrays, while the group attribute enables cross-list dragging between multiple containers.
  • Custom helper methods (deleteEle and pushEle) provide programmatic control for moving items between lists while preventing duplicates.
  • A setData workaround ensures Firefox compatibility by populating the dataTransfer object during drag operations.
  • Scoped SCSS styles for .sortable-chosen and .sortable-ghost provide visual feedback during drag interactions.

Frequently Asked Questions

How does vuedraggable integrate with Vue-Element-Admin's table components?

Vuedraggable integrates as a wrapper component around standard list rendering logic. In src/components/DndList/index.vue, the <draggable> element replaces a static container, binding directly to the list1 and list2 arrays via the :list prop. This integration maintains Vue's reactivity system while adding Sortable.js drag capabilities without modifying the underlying Element UI table components.

What is the purpose of the group attribute in the DndList component?

The group attribute creates a shared drag context between multiple draggable containers. By assigning both lists the group name article, the configuration allows items to move from list1 to list2 and vice versa. Without this attribute, dragging would be restricted to reordering within a single list. This pattern supports complex workflows like task boards or permission assignment interfaces.

How does the implementation handle browser-specific bugs like the Firefox drag issue?

The implementation includes a setData method that addresses a Firefox-specific requirement where the drag operation fails if the dataTransfer object contains no data. The method injects an empty string via dataTransfer.setData('Text', '') during the drag start event. This workaround ensures consistent cross-browser behavior without affecting the visual presentation or data payload of the dragged items.

Can the drag-and-drop pattern be extended to more than two lists?

Yes, the pattern scales to multiple lists by adding additional <draggable> components with the same group attribute value. Each list binds to a distinct array prop (e.g., list3, list4), and the helper methods can be refactored to accept target list parameters dynamically. The existing architecture in DndList demonstrates the reactive principles required for multi-list implementations, though the current component specifically handles two lists for the common "todo/done" workflow pattern.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →