Custom Element Transfer Component with Drag Sorting and Checkbox Interaction

Usage

Import and register the custom drag-enabled transfer component, then integrate it into your Vue template:

<CustomDragTransfer
  :selected-right-items="selectedRightFilterList"
  left-panel-title="Unavailable Filter Fields"
  right-panel-title="Saved Filter Presets"
  :all-source-data="fullFilterDataset"
  :field-mapping="transferFieldMap"
  @right-data-updated="updateSelectedFilters"
></CustomDragTransfer>

Component Props Explained

Prop Description
selected-right-items Pre-selected dataset rendeerd in the right panel
left-panel-title Custom display title for the left unselected list
right-panel-title Custom display title for the right selected list
all-source-data Complete source dataset shared between both panels
field-mapping Object mapping the component's value and label fields for list items
right-data-updated Emitted event that passes the updated right panel data to the parent component

Parent Component Data Definition

export default {
  data() {
    return {
      transferFieldMap: {
        value: "rfidSetId",
        label: "rfidSetName"
      }
    }
  }
}

Full Component Implemantation

Create the component file at src/components/CustomDragTransfer.vue:

<template>
  <div class="custom-drag-transfer">
    <div class="transfer-container">
      <!-- Left Panel -->
      <div class="transfer-panel left-panel">
        <div class="panel-header">
          {{ leftPanelTitle }}
          <span class="drag-hint">Drag fields to reorder or move between panels</span>
        </div>
        <div
          id="left-panel-list"
          class="transfer-list"
          @drop="handleDrop"
          @dragover="allowDrop"
        >
          <template v-for="(item, idx) in leftPanelData">
            <div
              :key="idx"
              draggable="true"
              @dragstart="handleDragStart($event, item)"
              :data-item-id="item[fieldMapping.value]"
              @click="toggleLeftItemSelection(item)"
              :class="[
                'transfer-list-item',
                item.checked ? 'active-selection' : ''
              ]"
            >
              <input
                class="item-checkbox"
                type="checkbox"
                v-model="item.checked"
                @change="handleLeftCheckboxChange(item)"
              />
              <span class="item-label">{{ item[fieldMapping.label] }}</span>
            </div>
          </template>
        </div>
      </div>

      <!-- Middle Action Buttons -->
      <div class="transfer-actions">
        <img
          src="@/assets/icons/arrow-right.svg"
          alt="Move selected items to right panel"
          @click="moveSelectedToRight"
          class="action-btn"
        />
        <img
          src="@/assets/icons/arrow-left.svg"
          alt="Move selected items to left panel"
          @click="moveSelectedToLeft"
          class="action-btn"
        />
      </div>

      <!-- Right Panel -->
      <div class="transfer-panel right-panel">
        <div class="panel-header">
          {{ rightPanelTitle }}
        </div>
        <div
          id="right-panel-list"
          class="transfer-list"
          @drop="handleDrop"
          @dragover="allowDrop"
        >
          <template v-for="(item, idx) in rightPanelData">
            <div
              :key="idx"
              draggable="true"
              @dragstart="handleDragStart($event, item)"
              :data-item-id="item[fieldMapping.value]"
              @click="toggleRightItemSelection(item)"
              :class="[
                'transfer-list-item',
                item.checked ? 'active-selection' : ''
              ]"
            >
              <input
                class="item-checkbox"
                type="checkbox"
                v-model="item.checked"
                @change="handleRightCheckboxChange(item)"
              />
              <span class="item-label">{{ item[fieldMapping.label] }}</span>
            </div>
          </template>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    selectedRightItems: {
      type: Array,
      default: () => []
    },
    leftPanelTitle: {
      type: String,
      default: "Unselected Items"
    },
    rightPanelTitle: {
      type: String,
      default: "Selected Items"
    },
    allSourceData: {
      type: Array,
      default: () => []
    },
    fieldMapping: {
      type: Object,
      default: () => ({
        value: "id",
        label: "name"
      })
    }
  },
  data() {
    return {
      draggedItem: null,
      leftPanelData: [],
      rightPanelData: []
    };
  },
  mounted() {
    this.initializePanelData();
  },
  watch: {
    allSourceData(newVal) {
      this.initializePanelData();
    },
    selectedRightItems(newVal) {
      this.syncRightPanelData(newVal);
    }
  },
  methods: {
    initializePanelData() {
      const rightIds = new Set(this.selectedRightItems.map(item => item[this.fieldMapping.value]));
      this.leftPanelData = this.allSourceData.filter(item => !rightIds.has(item[this.fieldMapping.value]));
      this.rightPanelData = [...this.selectedRightItems];
    },
    syncRightPanelData(rightItems) {
      const rightIds = new Set(rightItems.map(item => item[this.fieldMapping.value]));
      this.leftPanelData = this.allSourceData.filter(item => !rightIds.has(item[this.fieldMapping.value]));
      this.rightPanelData = [...rightItems];
    },
    allowDrop(e) {
      e.preventDefault();
    },
    handleDragStart(e, item) {
      this.draggedItem = item;
      e.dataTransfer.setData("text/plain", JSON.stringify(item));
    },
    handleDrop(e) {
      e.preventDefault();
      const targetPanel = e.currentTarget.id === "left-panel-list" ? "left" : "right";
      if (!this.draggedItem) return;

      if (targetPanel === "left") {
        this.moveItemToPanel(this.draggedItem, "left");
      } else {
        this.moveItemToPanel(this.draggedItem, "right");
      }
      this.draggedItem = null;
    },
    moveItemToPanel(item, targetPanel) {
      const sourcePanel = this.leftPanelData.includes(item) ? "left" : "right";
      if (sourcePanel === targetPanel) return;

      // Remove from source panel
      const sourceList = sourcePanel === "left" ? this.leftPanelData : this.rightPanelData;
      const itemIndex = sourceList.findIndex(i => i[this.fieldMapping.value] === item[this.fieldMapping.value]);
      if (itemIndex > -1) sourceList.splice(itemIndex, 1);

      // Add to target panel
      const targetList = targetPanel === "left" ? this.leftPanelData : this.rightPanelData;
      targetList.push(item);
      this.emitUpdatedData();
    },
    moveSelectedToRight() {
      const selectedItems = this.leftPanelData.filter(item => item.checked);
      selectedItems.forEach(item => this.moveItemToPanel(item, "right"));
      this.clearAllChecked();
    },
    moveSelectedToLeft() {
      const selectedItems = this.rightPanelData.filter(item => item.checked);
      selectedItems.forEach(item => this.moveItemToPanel(item, "left"));
      this.clearAllChecked();
    },
    toggleLeftItemSelection(item) {
      item.checked = !item.checked;
    },
    toggleRightItemSelection(item) {
      item.checked = !item.checked;
    },
    handleLeftCheckboxChange(item) {
      this.$emit("checkbox-change", { item, panel: "left" });
    },
    handleRightCheckboxChange(item) {
      this.$emit("checkbox-change", { item, panel: "right" });
    },
    clearAllChecked() {
      this.leftPanelData.forEach(item => item.checked = false);
      this.rightPanelData.forEach(item => item.checked = false);
    },
    emitUpdatedData() {
      this.$emit("right-data-updated", [...this.rightPanelData]);
    }
  }
};
</script>

<style scoped>
.custom-drag-transfer {
  padding: 16px;
  font-family: system-ui, sans-serif;
}
.transfer-container {
  display: flex;
  gap: 24px;
  align-items: center;
}
.transfer-panel {
  flex: 1;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  overflow: hidden;
}
.panel-header {
  padding: 12px;
  background-color: #f5f7fa;
  border-bottom: 1px solid #e4e7ed;
  font-weight: 500;
}
.drag-hint {
  font-size: 12px;
  color: #909399;
  margin-left: 8px;
}
.transfer-list {
  max-height: 400px;
  overflow-y: auto;
  padding: 8px;
}
.transfer-list-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  margin-bottom: 4px;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.2s;
}
.transfer-list-item:hover {
  background-color: #f5f7fa;
}
.active-selection {
  background-color: #ecf5ff;
  border-color: #d9ecff;
}
.item-checkbox {
  width: 16px;
  height: 16px;
}
.item-label {
  flex: 1;
  font-size: 14px;
}
.transfer-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.action-btn {
  width: 32px;
  height: 32px;
  cursor: pointer;
  transition: opacity 0.2s;
}
.action-btn:hover {
  opacity: 0.8;
}
</style>

Tags: vue Element UI Transfer Component drag-and-drop Checkbox Selection

Posted on Wed, 19 Aug 2026 16:16:58 +0000 by ceci