👌

Vue3.5で Vue.Draggable を使用してD&Dを簡単に実装する

に公開

はじめに

業務でプラグイン「Vue.Draggable」を使用してドラッグアンドドロップ機能を実装したのと、
Vueのv3.5で開発してみたかったので、備忘録として残します。

動作環境・使用するツールや言語

  • Vue: v3.5.13
  • Vuetify: v3.7.16
  • vuedraggable: v4.1.0

作成したリポジトリ

導入

公式ドキュメントに沿って進めます。

npm i -S vuedraggable@next

使用例

sample.vue
<script setup lang="ts">
import { ref } from 'vue'
import draggableComponent from 'vuedraggable'

const samples = ref([])
</script>

<template>
  <draggableComponent
    v-model="samples"
  >
    <template #item="{ element }">
      <div>{{ element.name }}</div>
    </template>
  </draggableComponent>
</template>

オプション設定

今回使用したオプション一覧です。
オプションはVue.Draggableが用意しているものと、
Vue.DraggableがサポートしているSortableのものがあります。

  • Vue.Draggable
    • item-key
      • 一意にするために設定する内部キー。
    • tag
      • ドラッグ可能なコンポーネントが含まれたスロットの外部要素として作成するHTMLノードタイプ。
  • Sortable
    • group
      • ドラッグ可能な対象をグループ化。
    • ghost-class
      • ドラッグ時の背景要素のクラス。
    • handle
      • ドラッグ要素をクラスで指定。(通常はスロットに含まれるすべての要素がドラッグ対象になる)
    • animation
      • ドラッグ時のアニメーション設定。

コンポーネント作成

以下のコンポーネントを作成しました。
ghost-classに指定しているクラスは、main.cssに定義しております。
FakeUserに関しては、余談として後ほど紹介します。

単一リスト

DraggableSinglePanel.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import draggableComponent from 'vuedraggable'

import { makeFakeUsers } from '@/helpers/fakeHelpers'

import type { FakeUser } from '@/types/fakes'

const users = ref<FakeUser[]>([])

onMounted(() => {
  users.value = makeFakeUsers(5)
})
</script>

<template>
  <v-container fluid>
    <v-row>
      <v-col cols="4">
        <div class="h-4">Group1</div>
      </v-col>
    </v-row>

    <v-row>
      <v-col cols="4">
        <draggableComponent
          v-model="users"
          item-key="id"
          class="draggable-handle"
          ghost-class="draggable-ghost"
          animation="200"
        >
          <template #item="{ element }">
            <v-card :title="element.name" prepend-icon="mdi-account" class="mt-4 mb-4">
              <v-card-item>
                <p>都道府県: {{ element.state }}</p>
                <p>郵便番号: {{ element.zipCode ?? '' }}</p>
              </v-card-item>
            </v-card>
          </template>
        </draggableComponent>
      </v-col>
    </v-row>
  </v-container>
</template>

複数リスト

グループを同一にすることで、列の横断が可能です。

DraggableMultiplePanel.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import draggableComponent from 'vuedraggable'

import { makeFakeUsers } from '@/helpers/fakeHelpers'

import type { FakeUser } from '@/types/fakes'

const users1 = ref<FakeUser[]>([])
const users2 = ref<FakeUser[]>([])
const users3 = ref<FakeUser[]>([])

onMounted(() => {
  users1.value = makeFakeUsers(5)
  users2.value = makeFakeUsers(5)
  users3.value = makeFakeUsers(5)
})
</script>

<template>
  <v-container fluid>
    <v-row>
      <v-col v-for="i in 3" :key="i" cols="3">
        <div class="h-4">Group{{ i }}</div>
      </v-col>
    </v-row>

    <v-row>
      <v-col cols="3">
        <draggableComponent
          v-model="users1"
          item-key="id"
          class="draggable-handle"
          ghost-class="draggable-ghost"
          animation="200"
          group="users"
        >
          <template #item="{ element }">
            <v-card :title="element.name" prepend-icon="mdi-account" class="mt-4 mb-4">
              <v-card-item>
                <p>都道府県: {{ element.state }}</p>
                <p>郵便番号: {{ element.zipCode ?? '' }}</p>
              </v-card-item>
            </v-card>
          </template>
        </draggableComponent>
      </v-col>
      <v-col cols="3">
        <draggableComponent
          v-model="users2"
          item-key="id"
          ghost-class="draggable-ghost"
          animation="200"
          group="users"
        >
          <template #item="{ element }">
            <v-card :title="element.name" prepend-icon="mdi-account" class="mt-4 mb-4">
              <v-card-item>
                <p>都道府県: {{ element.state }}</p>
                <p>郵便番号: {{ element.zipCode ?? '' }}</p>
              </v-card-item>
            </v-card>
          </template>
        </draggableComponent>
      </v-col>
      <v-col cols="3">
        <draggableComponent
          v-model="users3"
          item-key="id"
          ghost-class="draggable-ghost"
          animation="200"
          group="users"
        >
          <template #item="{ element }">
            <v-card :title="element.name" prepend-icon="mdi-account" class="mt-4 mb-4">
              <v-card-item>
                <p>都道府県: {{ element.state }}</p>
                <p>郵便番号: {{ element.zipCode ?? '' }}</p>
              </v-card-item>
            </v-card>
          </template>
        </draggableComponent>
      </v-col>
    </v-row>
  </v-container>
</template>

ドラッグ箇所のカスタム

handleプロパティにクラスを指定することで、ドラッグ箇所のカスタムが可能です。

DraggableCustomDragPanel.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import draggableComponent from 'vuedraggable'

import { makeFakeUsers } from '@/helpers/fakeHelpers'

import type { FakeUser } from '@/types/fakes'

const users = ref<FakeUser[]>([])

onMounted(() => {
  users.value = makeFakeUsers(5)
})
</script>

<template>
  <v-container fluid>
    <v-row>
      <v-col cols="4">
        <div class="h-4">Group1</div>
      </v-col>
    </v-row>

    <v-row>
      <v-col cols="4">
        <draggableComponent
          v-model="users"
          item-key="id"
          ghost-class="draggable-ghost"
          animation="200"
          handle=".draggable-handle"
        >
          <template #item="{ element }">
            <v-card class="mt-4 mb-4">
              <v-card-item :title="element.name">
                <template v-slot:prepend>
                  <v-icon icon="mdi-drag" class="draggable-handle" />
                </template>
                <p>都道府県: {{ element.state }}</p>
                <p>郵便番号: {{ element.zipCode ?? '' }}</p>
              </v-card-item>
            </v-card>
          </template>
        </draggableComponent>
      </v-col>
    </v-row>
  </v-container>
</template>

テーブル

tagにtbodyを指定することで、スロットの外部要素がtbodyとして出力されます。

DraggableTablePanel.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import draggableComponent from 'vuedraggable'

import { makeFakeUsers } from '@/helpers/fakeHelpers'

import type { FakeUser } from '@/types/fakes'

const users = ref<FakeUser[]>([])

onMounted(() => {
  users.value = makeFakeUsers(5)
})
</script>

<template>
  <v-container fluid>
    <v-table class="bg-grey-darken-3">
      <thead class="bg-grey-darken-4">
        <tr>
          <th>氏名</th>
          <th>都道府県</th>
          <th>郵便番号</th>
        </tr>
      </thead>
      <draggableComponent
        v-model="users"
        tag="tbody"
        item-key="id"
        class="draggable-handle"
        ghost-class="draggable-ghost"
        animation="200"
      >
        <template #item="{ element }">
          <tr>
            <td>{{ element.name }}</td>
            <td>{{ element.state }}</td>
            <td>{{ element.zipCode }}</td>
          </tr>
        </template>
      </draggableComponent>
    </v-table>
  </v-container>
</template>

余談

Vue.Draggableのバージョンについて

npmで「vuedraggable」と検索しても、バージョンが2.24.3と古いままで
どこに最新のバージョンがあるのかと疑問に思いました。
タグで管理されているようです。

FakeUserについて

Fakerというプラグインを使用してヘルパーを作成しました。
uuidというプラグインも使用して一意のidを生成しましたが、
Fakerにもuuidを生成する機能があったので、不要な導入でした💦

fakeHelpers.ts
import { fakerJA as faker } from '@faker-js/faker'
import { v4 as uuid } from 'uuid'

import type { FakeUser } from '@/types/fakes'

export const makeFakeUser = (): FakeUser => {
  return {
    id: uuid(),
    name: faker.person.fullName(),
    state: faker.location.state(),
    zipCode: faker.location.zipCode(),
  }
}

export const makeFakeUsers = (length: number): FakeUser[] => {
  return Array.from({ length }, makeFakeUser)
}

おわりに・まとめ

Vue.Draggableを使用することでD&D機能を簡単に実装できます。
もちろん自前で作成することも可能ではあります。
ただ、ドラッグ要素に複雑な機能の追加が発生した際に、自前で作成したD&Dが悪さをすることもあります。(ありました。)
便利なものはありがたく使用させていただきましょう。

ここまで読んでいただき、ありがとうございました。

Discussion