VueDraggableとLaravelを連携させる

VueDraggableとLaravelを連携させる

過去に Vue.js と Laravel を使用してタスク管理アプリを作成した記事を書きましたが、その中で VueDraggable と Laravel を連携させる実装に苦労したので、今回はその実装方法について書いていこうと思います。

実装した機能の動作

実装した機能は以下のような動きになります。移動できる要素をドロップした時点で、移動した要素の情報を非同期(axios を使用)で Laravel 側に渡して更新しています。

task_app

開発環境や使用しているプラグイン及びバージョン

  • PHP:7.4.15

  • Laravel:6.2

  • MySQL:8.0.21

  • Vue:2.6.12

  • Vuex:3.6.2

  • Vue-Router:3.5.1

  • VueDraggable:2.24.3

  • axios:0.21.1

ディレクトリ構成

※一部省略

resources
├── App.vue
├── app.js
├── bootstrap.js
├── router.js
├── statusCode.js
├── store/
│   ├── auth.js
│   ├── store.js
│   ├── error.js
│   ├── taskStore.js
│   ├── contact.js
│   ├── profile.js
├── components/
│   ├── Index.vue
│   ├── TaskApp.vue
│   ├── TaskCard.vue
│   ├── TaskFolder.vue
│   ├── Tasklist.vue
│   ├── twitterAuth/
│          ├── Callback.vue

縦移動を実装する

リストの縦移動を実装していきます。タスクの情報は Vuex を使用して管理しています。(Highlighting Code Block で Vue の表示方法が分からなかったので HTML と JS と分けています。。。)

TaskCard.vue
<draggable
  :list="cards.tasks"
  v-bind="getOptionsForTask()"
  @add="onAdd"
  @change="onChange"
  :data-card-id="cards.id"
>
  <TaskList
    v-for="(task, index) in cards.tasks"
    :key="task.id"
    :id="task.id"
    :title="task.title"
    :listIndex="index"
    :created_at="task.created_at"
    :cards="cards"
  />
</draggable>
TaskCard.vue

// タスクリストのソートを更新するアクションを呼ぶ
async updateTaskSort(newTasks) {
  await this.$store.dispatch("taskStore/updateTaskSort", newTasks);
  const folder_id = this.cards.folder_id;
  if (this.getCode === OK) {

  // 更新後データが更新されるので、選択されていたフォルダーを保持するための処理
  await this.$store.dispatch("taskStore/setCardListsAction",folder_id);
  }
},
async updateTaskDraggable(cardId, taskId) {
  await this.$store.dispatch("taskStore/updateTaskDraggable", {
    card_id: cardId,
    task_id: taskId,
  });
},
async updateTaskSort(newTasks) {
  await this.$store.dispatch("taskStore/updateTaskSort", newTasks);
  const folder_id = this.cards.folder_id;
  if (this.getCode === OK) {
  await this.$store.dispatch("taskStore/setCardListsAction",folder_id);
  }
},
onAdd(e) {
  console.log(e.from)
  console.log(e.to)
  console.log(e.item)
  let taskId = e.item.getAttribute("data-task-id");
  let toCardId = e.to.getAttribute("data-card-id");
  this.updateTaskDraggable(toCardId, taskId);
},
onChange() {
  let newTasks = this.cards.tasks.map((task, index) => {
    task.priority = index + 1;
    return task;
  });
  this.updateTaskSort(newTasks);
},
taskStore.js

async updateTaskSort({ commit }, newTask) {
  // 移動後の新しい状態のリストをLaravel側に渡す
  const response = await axios.patch("/api/task/update-all", {
    tasks: newTask,
  });
  if (response.status === UNPROCESSABLE_ENTITY) {
    commit("setTaskRequestErrorMessages", response.data.errors);
  } else {
    commit("error/setCode", response.status, { root: true });
  }
},
// タスクリストの列の入れ替え更新
async updateTaskDraggable({ commit }, { card_id, task_id }) {
  const response = await axios.put("/api/task/" + task_id, card_id);
  if (response.status === UNPROCESSABLE_ENTITY) {
    commit("setTaskRequestErrorMessages", response.data.errors);
  } else {
    commit("error/setCode", response.status, { root: true });
},

VueDraggable は SortableJS というライブラリを Vue.js 用に移植されたもので、SortableJS のイベントを使用することが出来ます。

今回は add イベントと change イベントを使用しています。

add イベント:リストが移動先にドロップされ、データが追加された時に発火

change イベント:要素のドラッグ位置が変更された時に発火

add イベントでは、リストがカード間を移動(横移動)してデータが他のカードに追加された際に発火します。イベントの item プロパティには移動してきた要素の情報が含まれており、to プロパティには移動後の HTML 要素の情報が含まれています。

画像は上から console.log(e.from) console.log(e.to) console.log(e.item)を出力した結果です。

デバッグ

要素の移動先を追跡する

リロードされた時にリストの状態を保持しないのであれば特に意識する必要は無いですが、データベースに移動後の情報を登録するには移動したリストを追跡する必要があります。

その実装方法として、タスクやカードの ID を情報として持たせてドラッグ&ドロップをした際にその ID を渡して移動元と移動先を判別するようにしました。

デバッグ

移動してきた要素をデータベースに登録する

タスクやカードの情報は、id のほかに priority を持たせています。

データベース側では priority カラムを持たせており、初期値は NULL にしています。

縦の移動が行われた時点で Vue.js 側で priority プロパティの情報を更新し、更新後の情報をデータベースに登録するという流れで実装しています。

TaskController.php
public function updateTaskSort(Request $request)
{
  $newTasks = $request->tasks;

  try {
    // タスクをすべて取得
    $tasks = Task::all();

    // DBに登録されているタスクをループで分解
    foreach ($tasks as $task) {
      foreach ($newTasks as $newTask) {
      // 既存のタスクIDと更新後のタスクIDが同じだったら既存タスクのソート順を更新する
      if ($newTask["id"] === $task->id) {
        $task->update(["priority" => $newTask["priority"]]);
      }
    }
  }
   return response(["success"], 200);
  } catch (\Exception $e) {
  \Log::debug("予期せぬエラーが発生しました。" . $e->getMessage());
    return response()->json(["errors", "エラーが発生しました。"],500);
  }
}

データベースに登録されている既存の情報と、Vue 側から渡されたタスクのリストの ID を比較して同じであれば priority カラムを更新します。

こうすることで Vue 側で並び替えた要素をデータベース側にも反映することが出来ます。

更新後、データを取得する際は priority カラムを基準としてデータをソートして取得するように実装いています。

タスクフォルダーをクリックした際に以下の処理が走ります。タスクリストをソートしてから、カードをソートしています。

FolderController.php
// フォルダー配下のカードのデータを取得
public function selectCrad($folder_id)
{
  $user = Auth::user();

  $user_id = $user->id;

  // タスクリストをソートする処理
  $allData = User::with(["folders.cards.tasks" => function ($query) {
    $query->orderBy("priority", "asc");
  },])->find($user_id);

  // カードをソートする処理
  $folderData = $allData->folders->find($folder_id)->cards->sortBy('priority')->values()->all();
  return $folderData;
}

横移動を実装する

タスクリストを別のカードに移動する場合は、タスクの ID とカードの ID を渡してその情報を元にデータベースを更新するように実装しました。

$request にはカード ID が渡って来ており、現在保存されているタスクの情報の card_id を更新することで横移動した結果をデータベースに反映しています。

phpmyadmin

public function updateTaskDraggable(Request $request, $task_id)
{
  try {
    $task = Task::find($task_id);
    $task->update($request->all());
    return $task;
  } catch (\Exception $e) {
    \Log::debug("予期せぬエラーが発生しました。" . $e->getMessage());
    return response()->json(["errors", "予期せぬエラーが発生しました。"], 500);
  }
}

まとめ

今回はフロント側で並び替えた情報をデータベースへ反映するという処理に苦戦しましたが、何とか形として実装する事ができました。

SortableJS は React に適応したものもあるみたいなので、そちらも試してみたいと思います。

不格好なコードかもしれませんが、参考になれば幸いです!

参考

https://techblog.roxx.co.jp/entry/2018/12/18/120000