Re:Vue

ドラッグ&ドロップでトレロ!


Vue-draggable-next


1. はじめに

LaravelとVue.jsを学習している私が一番にやりたかったことは、ドラッグ&ドロップができるトレロを作ってみること。tech-pitというサイトでVue.jsとVuexを使ったドラッグ&ドロップできるトレロを、Laravelでドラッグ&ドロップできないけど、DB連携しているトレロを作ってみたことがあって、DB連携しつつ、ドラッグ&ドロップができるトレロが作れないかと思ったのがきっかけ。双方のフレームワークの良さや使い方を知るのに時間を有したが、満を持して2022年6月に挑戦してみた。


2. 基本構文

HTMLのmetaタグに <meta name="csrf-token" content="{{ csrf_token() }}">を加えておく。私の場合は下記のように、共通テンプレートにそれを記述した。

■ まずはインストール

   npm install -S vuedraggable@next

▼ package.jsonを確認

    "dependencies": {
        "vuedraggable": "^4.1.0",
    }

■ 基本的な書き方

<draggable>タグにv-modelで、バインドさせたい配列またはオブジェクトをセットする。 Vue2仕様のvuedraggable(前バージョン)のようにループを書かずとも、v-modelでセットした配列やオブジェクトがループするようになっている。

<template>
  <draggable v-model="myArray" group="people" item-key="id">
    <template #item="{element}">
      <div>{{element.name}}</div>
    </template>
  </draggable>
</template>
<script>
import draggable from "vuedraggable";

export default {
    components: {
        draggable
    },
}

3. Laravel×Vueでドラッグ&ドロップ

コードが長くなるので、該当部分だけを抜粋しながら、整理。


■ ビュー/コンポーネント

▼ resources\views\listings\index.blade.php


   @extends('layouts.login')
    @section('content')
    <div id="app">
       <App :csrf="{{json_encode(csrf_token())}}"></App>
    </div>
    //resources/assets/js/app.jsで書いたVueの設定を読み込む
    <script src="{{ asset('js/app.js') }}" defer="defer"></script>
    @endsection

トレロ風のかんばんボードの実装に使えるコード。列とカードは「1」対「多」の関係で、1つの列につき多くのカードを有することが出来る状態とする。なので、列の数だけループさせて、列の中にあるカードの数だけループさせるという二重ループとなる。(カードの方に列の外部キーをカラムとして持っているという想定)

▼ resources\assets\js\components\App.vue(抜粋)

<template>
  <section id="kanban">
    <draggable
      tag="div"
      class="kanban-main"
      v-model="state.lists"
      item-key="id"
      @end="listSort"
    >
      <template #item="{ element }">
        <div class="list-item">
          <div class="list-title">{{ element.title }}</div>

          <draggable
            v-model="element.cards"
            item-key="index"
            group="items"
            @end="cardSort"
          >
            <template #item="{ element }">
              <div class="card-item">
                <div class="card-deadline">
                  {{ element.deadline }}
                </div>
                <div class="card-title">
                  {{ element.title }}
                </div>
              </div>
            </template>
          </draggable>
        </div>
      </template>
    </draggable>
  </section>
</template>
<script>
import { reactive, onMounted } from "vue";
import draggable from "vuedraggable";
import axios from "axios";

export default {
  name: "app",
  components: {
    draggable,
  },
  props: {
    csrf: {
      type: String,
      required: true,
    },
  },
  //データ取得
  setup(props) {
    const state = reactive({
      lists: [],
    });


    //列の並べ替え
    const listSort = () => {
      //idとsortのJSONデータを生成
      const lists = {};
      lists.id = [];
      for (let i = 0; i < state.lists.length; i++) {
        lists.id.push(state.lists[i].id);
      }
      axios.post("listings/sort", lists).then((response) => {
        // console.log(response);
      });
    };

    //カードの並べ替え
    const cardSort = () => {
      const lists = {};
      lists.data = [];
      for (let i = 0; i < state.lists.length; i++) {
        if (state.lists[i].cards.length > 0) {
          for (let j = 0; j < state.lists[i].cards.length; j++) {
            lists.data.push({
              list_id: state.lists[i].id,
              card_id: state.lists[i].cards[j].id,
            });
          }
        }
      }
      axios.post("cards/sort", lists).then((response) => {
        // console.log(response);
      });
    };

    //表示
    async function main() {
      state.info = await axios
        .get("main")
        .then(function (res) {
          // console.log(res.data);
          state.lists = res.data;
        })
        .catch(function (err) {
          console.log(err);
        });
    }
    //mount
    onMounted(() => {
      main();
    });

    return {
      state,
      main,
      ・・・
      listSort,
      cardSort,
    };
  },
};
</script>
<style>
</style>

カードの<draggable>タグにgroupを付けることで、列間のカード移動が簡単に実装できる。 @endはドラッグ&ドロップし終わった後にイベントを呼び出すことができる。ここでは、列やカードが移動した時に、並べ替えの処理が動くようになっている。


列の並べ替え

列の数だけループをする。列が並んでいる順番にIDをJSONデータ(風)に格納している。そのデータをコントローラーに渡している。

    const listSort = () => {
      //idとsortのJSONデータを生成
      const lists = {};
      lists.id = [];
      for (let i = 0; i < state.lists.length; i++) {
        lists.id.push(state.lists[i].id);
      }
      axios.post("listings/sort", lists).then((response) => {
        console.log(response);
      });
    };

カードの並べ替え

カードも列と理屈は同じで列の並び順にループし、それぞれの列の中もカードが並んでいる順番にループする。列のIDとそれに属するカードのIDをセットでデータを作って、コントローラーに渡している。

    const cardSort = () => {
      const lists = {};
      lists.data = [];
      for (let i = 0; i < state.lists.length; i++) {
        if (state.lists[i].cards.length > 0) {
          for (let j = 0; j < state.lists[i].cards.length; j++) {
            lists.data.push({
              list_id: state.lists[i].id,
              card_id: state.lists[i].cards[j].id,
            });
          }
        }
      }
      axios.post("cards/sort", lists).then((response) => {
        // console.log(response);
      });
    };

■ ルーティング

Vue.js側でデータをLaravel側に送れるようになったところで、まずはルーティング。並べ替え処理用のメソッドを用意する。

▼ routes\web.php


//リスト並べ替え処理
Route::post('listings/sort','ListingsController@sort');

//カード並べ替え処理
Route::post('cards/sort','CardsController@sort');

■ コントローラー

Vueから送られてきたデータはソート順に並んでいるので、テーブルにソート用カラムを用意するなどして、 並び順を保存する。ステータスコードを返してあげると親切。

▼ app/Http/Controllers/ListingsController.php

<?php

namespace App\Http\Controllers;

use App\Listing;
use \Symfony\Component\HttpFoundation\Response;
use Illuminate\Http\Request;

class ListingsController extends Controller
{

  ・・・

    public function sort(Request $request) {
        try {
            for($i=0;$i<count($request->id);$i++){
                $listing = Listing::find($request->id[$i]);
                $listing->sort = (string) $i;
                $listing->save();
            };
           //ステータスコード
            $message="SUCCESS";
            $http_status = Response::HTTP_OK;

        } catch (\Exception $e) {
            $e->getMessage();
            $message="DB ERROR!";
            $http_status = Response::HTTP_INTERNAL_SERVER_ERROR;
        }

        return response()->json($message, $http_status);
    }
}

▼ app/Http/Controllers/CardsController.php(抜粋)

<?php

namespace App\Http\Controllers;

use App\Card;
use Illuminate\Http\Request;
use \Symfony\Component\HttpFoundation\Response;

class CardsController extends Controller
{

    ・・・

    public function sort(Request $request) {
        try {
            for($i=0; $i < count($request->data); $i++){
                $cards = Card::find($request->data[$i]['card_id']);
                $cards->listing_id = $request->data[$i]['list_id'];
                $cards->sort = (string) $i;
                $cards->save();
            }
            //ステータスコード
            $message="SUCCESS";
            $http_status = Response::HTTP_OK;
        } catch (\Exception $e) {
            $e->getMessage();
            $message="DB ERROR!";
            $http_status = Response::HTTP_INTERNAL_SERVER_ERROR;
        }

        return response()->json($message, $http_status);
    }

}

4.おわりに

今は公開していない(2022/06/15時点)Yuu’s MemoでLaravelとVue.jsを使ってトレロを作っているのを見て、自分も作ってみたいと思った(その前にLaravelだけでドラッグ&ドロップの出来ないトレロは作っていた)。下記のサイトを参考にしつつ、vue.draggableの書き方がVue2と3で変わっているじゃないかという文句を言いつつ、ついに実装出来た。これはかなりの達成感があり、友人・知人に出来た!と言っては見せびらかしたとさ。