【Rails7 + Vue3】Vue.Draggable の使い方

## 初めに

フロントは`Vue.js`で開発し、サーバーサイドは`Rails`を使用しているアプリに
ドラッグ&ドロップ機能を追加するため、`Vue.Draggable`を導入した時のお話です。

コードはかなり端折っていますが、実装の流れを備忘録で残します。
`Draggable`の`github`は[こちら](https://github.com/SortableJS/Vue.Draggable)にあります。 

## 環境
ruby 3.1.0
Rails 7.0.4
vue@3.2.41
## インストール

著者は`importmap`を使用しているので以下のコマンドで`pin`します。
そうすると、`importmap.js`に自動書き込みが走ります。
```
./bin/importmap pin vuedraggable@next --download
```

## 使い方
その後、`draggable`を使用可能にするために読み込み、
コンポーネントとして使用できるようにします。
```js
import Draggable from "vuedraggable";

// 略
 components: {
    Draggable,
  },

```
使い方は簡単で以下のように、ドラッグさせたいコンポーネントを `<draggable>` で
挟むことで簡単にドラッグ＆ドロップが実装できます。

vue3系からは　`<template #item>`を`draggable`タグの中に仕込まないと
エラーが発生するので注意が必要です。
```vuejs
// 略
<draggable>
  <template #item="{ element }">
    <!-- ★ここにドラッグ&ドロップさせたいコンポーネントを挿入します -->
  </template>
</draggable>

<script>
import Draggable from "vuedraggable";

components: {
    draggable,
  },
}
```


今回は以下のように `v-for` 構文で`Cards`を1個ずつ表示している状態で、
`card`同士を自由にドラッグできるようにすることをゴールにします。

```vuejs
       
<template v-for="card in Cards">
  <template #item="{ element }">
    <div class="card">
      <p class="title">{{ card.title }}</p>
    </div>
  </template>
</template>

```
以下のように追記します。
`Cards`を`:list`に渡すことで、`draggable`の内部で自動でループ処理が行われます。
`<template #item>`タグ内の`element`の中に、配列の個別データが格納されます。

また、`item-key`にループ要素の`key`になる値を設定します。
これがないと、`warning`が出ます。

```vuejs
<draggable :list="Cards" item-key="id"> // 追記
  <template #item="{ element }">  // 追記
    <div class="card">
      <p class="title">{{ element.title }}</p>
    </div>
  </template>
</draggable>

```


この段階でもそれっぽい動きが可能でドラッグ＆ドロップができます。

しかし、並び替えて新しくなった順番は保存できていないため、
ドラッグ後、リロードすると元の順番に戻ってしまいます。

`card`を並び替えた状態で保存できた方が便利ですね。

`card`を並び替えた状態で保存するためにまず
①ドラッグの完了を補足　→ ②DBに順番を保存する 必要があります。

## ①ドラッグの完了を補足

まずは①について見ていきます。
`Vue.Draggable`はありがたいことに用意されているイベントが多く、その中の`end`を使います。
これは、ドラッグ＆ドロップが終わったタイミングで発火します。

ちなみにイベントは[こちらのページ](https://www.kabanoki.net/9532/#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88)で綺麗にまとめられていました。
ありがてぇ


```vuejs
<draggable :list="Cards"
           @end="saveCardmIndex()"> // ここを修正
  <template #item="{ element }">
    <div class="card">
      <p class="title">{{ element.title }}</p>
    </div>
  </template>
</draggable>

```

## ②DBに順番を保存する
次に②について見ていきます。
**結論、順番のデータを保持するためのカラムを用意しました。**

`Card`テーブルに新しく`sort`カラムを追加します。
```
rails g migration AddSortToCard sort:integer
```

マイグレーションファイルは以下のような感じ

```ruby
class AddSortToCard < ActiveRecord::Migration[7.0]
  def change
    add_column :card, :sort, :integer
  end
end
```
その後、`rails db:migrate`コマンドでDBに反映させます。


では実際の実装イメージを書いていきます。

ドラッグ＆ドロップする前が以下のような並びになっていると仮定します。

```
array = {
  0: {id: 1, title: '111', sort: 0}
  1: {id: 2, title: '222', sort: 1}
  2: {id: 3, title: '333', sort: 2}
}
```
ドラッグ＆ドロップすると以下のようにデータが入れ替わると仮定します。
```
array = {
  0: {id: 1, title: '111', sort: 0}
  1: {id: 3, title: '333', sort: 2}
  2: {id: 2, title: '222', sort: 1}
}
```
上記のように入れ替わった後、その時の`index`番号で`sort`カラムを書き換えようという作戦です。

以下にざっくりした設計を記載します。

>ドラッグが完了
↓
ドラッグの並びになったデータすべて（配列）をpost送信する
↓
controllerで配列を受け取り`each_with_index`で`sort`カラムを更新
↓
リロード時にsortカラムの順番に整形して画面に返却する


では順番に書いていきます。
### ドラッグの並びになったデータすべて（配列）をpost送信する
上記にも書いた通りで、ドラッグ＆ドロップ終了時に、全ての`card`情報が詰まった配列を`post`送信します。

```vuejs
<draggable :list="Cards"
           @end="saveCardIndex()">
  <template #item="{ element }">
    <div class="card">
      <p class="title">{{ element.title }}</p>
    </div>
  </template>
</draggable>

<script>
  methods: {
    saveCardIndex() {
      Axios.post("update_card_index", cards: this.Cards)
    },
```

※`routes.rb`の変更もお忘れなく

### controller側で、配列を受け取り `each_with_index`で`sort`カラムを更新

`post`送信された`card`配列を受け取って、`each_with_index`によって`card`ごとに`sort`情報を更新します。これで、画面の状態をDBに保存することができました。
```ruby
# 略
  def update_sort
    # 全てのcardのsortを更新
    card_array_params.each_with_index do |card,i|
      card = Card.find(card.id)
      card.sort = i
      card.save!
    end
    
    render json: {result: "ok"}
  end

  private

  # オブジェクトの配列で送信されるため、一個ずつ中身の検査が必要
  def card_array_params
    params.require(:_json).map do |param|
      param.permit(:id)
    end
  end 
```


### リロード時に、sortカラムの順番に整形して画面に返却する
`.order(sort: :asc)`を追加することで、画面に`get`アクセスした時や、リロードした時に`sort`カラムの順番で画面に表示されるようになります。

```cards_controller.rb
#省略
  def index
    cards = Card.all.order(sort: :asc) # ここを修正
    render json: cards, methods: :language
  end
#省略
```
これで、ドラッグ＆ドロップした情報がDBに保存され、リロードにも対応できるようになりました。
ドラッグ中のアニメーションなど置いといて、最低限の機能が実装できました。

## 最後に

ここまで読んでくださりありがとうございました。
実装方法はかなり簡単で、イベントも豊富なのでかなり気合いの入ったプラグインだと感じました。
サクッとドラッグ＆ドロップを作りたいときに便利ですね。

## 参考
▼アニメーションをつけるなら
[ドラッグ時の掴める箇所やアニメーションを設定](https://www.kabanoki.net/9532/#%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E6%99%82%E3%81%AE%E6%8E%B4%E3%82%81%E3%82%8B%E7%AE%87%E6%89%80%E3%82%84%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%A8%AD%E5%AE%9A)