ライブラリを利用することなく短時間でシンプルなコードを利用してドラッグ&ドロップでテーブル行の並び替えをしたいと思ったことはないですか?本文書ではVue.jsのCompositions API環境でドラッグ&ドロップを利用してテーブル行の並び替えを行う方法を説明しています。実装にはdragstart, dragend, dragover, dragenterイベントを利用しているのでVue.jsでのdragイベントに馴染みがない人のdragイベントの学習にも利用することができます。

動作確認はmacOSを利用、Vue.jsのバージョンは3.2です。

プロジェクトの作成

npm init vue@latestコマンドを利用してVue.jsのプロジェクトを作成します。コマンド実行後はプロジェクト名の設定とプロジェクトで利用する機能の選択を行います。本文書ではどの機能も選択せずプロジェクトの作成を行います。プロジェクトの名前は任意の名前をつけることができます。vue-table-row-reorderという名前をつけています。


 % npm init vue@latest

プロジェクトの作成後は作成されたフォルダvue-table-row-reorderに移動します。


 % cd vue-table-row-reorder

UserTable.vueファイルのの作成

srcフォルダに存在するcomponentsフォルダの下にUserTable.vueファイルを作成します。作成後以下のコードを記述します。


<script setup></script>

<template>
  <div>UserTable</div>
</template>

srcフォルダのApp.vueファイルを開いて作成したUserTableコンポーネントをimportします。


<script setup>
import UserTable from './components/UserTable.vue';
</script>

<template>
  <UserTable />
</template>

npm run devコマンドで開発サーバを起動してUserTableコンポーネントの内容が表示されるか確認します。


 % npm run dev
UserTableコンポーネントの内容を確認
UserTableコンポーネントの内容を確認

データの取得

並び替えに利用するデータは無料で利用できるJSONPlaceHolderを利用します。https://jsonplaceholder.typicode.com/usersにアクセスすると10名分のユーザ情報を取得することができます。どのようなデータが取得できるかはブラウザからでも確認できます。

取得したデータを保存するためにリアクティブな変数usersをref関数を利用して定義します。ref関数を利用するためにrefをimportする必要があります。

データの取得は非同期関数のgetUsersを追加します。scriptタグの中で追加したgetUsersを実行するとコンポーネントのマウント時に実行されます。取得したデータはusersに保存し、v-forディレクティブを利用して展開します。v-forで設定している配列の要素番号の情報を持つindexは後ほど利用します。


<script setup>
import { ref } from 'vue';

const users = ref([]);

const getUsers = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await response.json();
  users.value = data;
};

getUsers();
</script>

<template>
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>名前</th>
        <th>ユーザ名</th>
        <th>Email</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(user, index) in users" :key="user.id">
        <td>{{ user.id }}</td>
        <td>{{ user.name }}</td>
        <td>{{ user.username }}</td>
        <td>{{ user.email }}</td>
      </tr>
    </tbody>
  </table>
</template>

ブラウザで確認すると10名分のユーザ情報が一覧で表示されます。

ユーザ一覧の表示
ユーザ一覧の表示

テーブルには罫線が表示されていないのでstyleタグを追加して設定を行います。

UserTable.vueファイルに以下を追加します。


<style>
table,
th,
td {
  border: 1px solid black;
  border-collapse: collapse;
  padding: 1em;
}
</style>

styleタグをUserTable.vueファイルに追加後再度ブラウザを確認するとテーブルには罫線がついて表示されます。

罫線付きのテーブルの表示
罫線付きのテーブルの表示

テーブル行の並び替えを実装するための準備は完了です。

並び替え処理

draggableの設定

現在の設定では行の要素をマウスで掴みドラッグを行うことはできません。要素をドラッグできるようにするためにはdraggable属性を設定する必要があります。tr要素にdraggable属性を設定します。


<tr v-for="(user, index) in users" :key="user.id" :draggable="true">
  <td>{{ user.id }}</td>
  <td>{{ user.name }}</td>
  <td>{{ user.username }}</td>
  <td>{{ user.email }}</td>
</tr>

設定後は移動した要素の上にマウスを移動して左ボタンで要素を掴むことができるようになります。ボタンを押している間要素をドラッグすることができます。下記の図はIDを10を持つtr要素をドラッグしています。

tr要素をドラッグ
tr要素をドラッグ

ドラッグした要素上で押していたボタンを外すとその場所に要素が追加できるように設定を行なっていきます。

dragstartイベントの設定

要素を掴んでドラッグを開始した直後に発火するdragstartイベントの設定を行います。dragstartイベントがドラッグ直後に発火するのかどうか確認を行います。dragstartイベントは@dragstartで取得することができイベントが発火したらdragStart関数を実行します。


<tr
  v-for="(user, index) in users"
  :key="user.id"
  :draggable="true"
  @dragstart="dragStart"
>
  <td>{{ user.id }}</td>
  <td>{{ user.name }}</td>
  <td>{{ user.username }}</td>
  <td>{{ user.email }}</td>
</tr>

設定したdragStart関数を定義します。


const dragStart = () => {
  console.log('drag start');
};

要素をドラッグしてブラウザのデベロッパーツールのコンソールに”drag start”が表示されることを確認してください。

確認できたら次はdragstartイベントでドラッグした要素のindex(配列の要素番号)を保存します。indexを保存するためにref関数でdragIndexを定義します。初期値をnullに設定しています。


const users = ref([]);
const dragIndex = ref(null);

dragstartイベントが発火した際にindexの情報を取得するためにdragStart関数の引数にindexを指定します。


<tr
  v-for="(user, index) in users"
  :key="user.id"
  :draggable="true"
  @dragstart="dragStart(index)"
>
  <td>{{ user.id }}</td>
  <td>{{ user.name }}</td>
  <td>{{ user.username }}</td>
  <td>{{ user.email }}</td>
</tr>

dragStart関数では引数で取得したindexを受け取ることができるので受け取ったindexにdragIndexに保存します。


const dragStart = (index) => {
  console.log('drag start', index);
  dragIndex.value = index;
};

設定後、要素をドラッグすると”drag start”の文字列の横にドラッグした要素のindexが表示されることを確認してください。indexは配列の要素番号なのでドラッグした要素によって0から9の値が表示されます。

dragstartイベントの設定は完了したのでconsole.logの行は削除しておきます。

dragenterイベントの設定

ドラッグした要素が別の要素に入った時に発火するdragenterイベントをtr要素に設定します。dragenterイベントが発火されるとdragEnter関数が実行され引数にはindexを指定しています。indexにはドラッグした要素が入ってきた要素のindexが入ります。ドラッグしている要素のindexではありません。


<tr
  v-for="(user, index) in users"
  :key="user.id"
  :draggable="true"
  @dragstart="dragStart(index)"
  @dragenter="dragEnter(index)"
>
  <td>{{ user.id }}</td>
  <td>{{ user.name }}</td>
  <td>{{ user.username }}</td>
  <td>{{ user.email }}</td>
</tr>

dragEnter関数でドラッグした要素のindexが保存されているdragIndexとドラッグした要素が入ってきた要素のindexを確認します。


const dragEnter = (index) => {
  console.log('index', index);
  console.log('dragIndex', dragIndex.value);
};

同じ要素をドラッグしている間はdragIndexは同じ値が表示され、tr要素を超えていく度に異なるindexがコンソールに表示されることを確認してください。

確認ができたら、indexとdragIndexの値が異なる場合のみ要素の入れ替えを行う処理を追加します。


const dragEnter = (index) => {
  if (index === dragIndex) return;
  const deleteElement = users.value.splice(dragIndex.value, 1)[0];
  users.value.splice(index, 0, deleteElement);
  dragIndex.value = index;
};

spliceメソッドとdragIndexを利用してドラッグした要素をusersの配列から削除しています。削除した要素(ドラッグした要素)がspliceメソッドの戻り値の配列番号の0に入っているのでdeleteElementに保存しています。再度spliceメソッドを実行して今度はindexの場所に削除した要素を追加しています。これで入れ替え処理を行うことができます。

spliceメソッドの処理の詳しい内容についてはMDNなどで確認してください。

配列を入れ替えた後はdragIndexにドラッグした要素が入れ替わった後のindexを設定しています。

これで並び替えの処理は完了です。一番上のIDが1の要素をドラッグして一番下の要素に移動できるか確認してください。

一番上の要素を一番下に移動した場合
一番上の要素を一番下に移動した場合

dragoverイベント

OSやブラウザによってドラッグで要素を移動した後にドロップする(並び替えは完了)と半透明の要素が元の場所に戻るような動作をします。その動作を止めるためにdragoverイベントを設定します。dragoverイベントではデフォルトの処理を停止するためpreventDefaultを実行します。dragoverイベントでpreventDefaultを実行するとドラッグした要素をドロップしても半透明の要素が元の場所に戻るという動作がなくなります。


<tr
  v-for="(user, index) in users"
  :key="user.id"
  :draggable="true"
  @dragstart="dragStart(index)"
  @dragenter="dragEnter(index)"
  @dragover.prevent
>
  <td>{{ user.id }}</td>
  <td>{{ user.name }}</td>
  <td>{{ user.username }}</td>
  <td>{{ user.email }}</td>
</tr>

dragendイベント

ここではJSONPlaceHolderを利用しているのでデータの取得を行うことはできますがデータの保存を行うことができません。実際のアプリケーションでは並び替えを行なった後のデータを保存する処理が必要になります。データ保存の処理を行う場所としてdragendイベントを利用します。

dragendイベントはドロップした際に発火されます。


<tr
  v-for="(user, index) in users"
  :key="user.id"
  :draggable="true"
  @dragstart="dragStart(index)"
  @dragenter="dragEnter(index)"
  @dragover.prevent
  @dragend="dragEnd"
>
  <td>{{ user.id }}</td>
  <td>{{ user.name }}</td>
  <td>{{ user.username }}</td>
  <td>{{ user.email }}</td>
</tr>

dragEnd関数を追加し、ドラッグを止めた(ドロップ)時にイベントが発火するか確認します。


const dragEnd = () => {
  console.log('drop');
};

ドラッグのために押していたボタンを離した時にコンソールに”drop”が表示されることを確認してください。

並び替え後のデータをサーバに送信する等の処理をdragEnd関数の中に記述します。dragEndが実行される時は並び替えの処理が完了しているのでdragIndexの値を初期値のnullに戻しておきます。


const dragEnd = () => {
  // 並び替え後のusers配列をサーバに送信する処理を追加する
  dragIndex.value = null;
};

ドラッグ要素を目立たせる

ドラッグした要素を目立たせるためにドラッグ要素にスタイルを設定します。

indexとdragIndexの値が等しい時にclassのdraggingを適用します。


{users.map((user, index) => (
  <tr
    key={user.id}
    draggable={true}
    onDragStart={() => dragStart(index)}
    onDragEnter={() => dragEnter(index)}
    onDragOver={(event) => event.preventDefault()}
    onDragEnd={dragEnd}
        className={index === dragIndex ? 'dragging' : ''}
  >
    <td>{user.id}</td>
    <td>{user.name}</td>
    <td>{user.username}</td>
    <td>{user.email}</td>
  </tr>
))}

draggingクラスはscriptタグに追加します。


<script>
table,
th,
td {
  border: 1px solid black;
  border-collapse: collapse;
  padding: 1em;
}

.dragging {
  background-color: #eee;
}
</script>

ドラッグしている要素にスタイルを適用することでドラッグしている要素が先ほどよりもわかりやすくなりました。

ドラッグした要素にスタイルを適用
ドラッグした要素にスタイルを適用

作成したUserTable.vueファイルは下記の通りです。


<script setup>
import { ref } from 'vue';

const users = ref([]);
const dragIndex = ref(null);

const getUsers = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await response.json();
  users.value = data;
};

getUsers();

const dragStart = (index) => {
  dragIndex.value = index;
};

const dragEnter = (index) => {
  if (index === dragIndex) return;
  const deleteElement = users.value.splice(dragIndex.value, 1)[0];
  users.value.splice(index, 0, deleteElement);
  dragIndex.value = index;
};

const dragEnd = () => {
  // 並び替え後のusers配列をサーバに送信する処理を追加する
  dragIndex.value = null;
};
</script>

<template>
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>名前</th>
        <th>ユーザ名</th>
        <th>Email</th>
      </tr>
    </thead>
    <tbody>
      <tr
        v-for="(user, index) in users"
        :key="user.id"
        :draggable="true"
        @dragstart="dragStart(index)"
        @dragenter="dragEnter(index)"
        @dragover.prevent
        @dragend="dragEnd"
        :class="index === dragIndex ? 'dragging' : ''"
      >
        <td>{{ user.id }}</td>
        <td>{{ user.name }}</td>
        <td>{{ user.username }}</td>
        <td>{{ user.email }}</td>
      </tr>
    </tbody>
  </table>
</template>

<style>
table,
th,
td {
  border: 1px solid black;
  border-collapse: collapse;
  padding: 1em;
}
.dragging {
  background-color: #eee;
}
</style>