初めに
フロントはVue.js
で開発し、サーバーサイドはRails
を使用しているアプリに
ドラッグ&ドロップ機能を追加するため、Vue.Draggable
を導入した時のお話です。
コードはかなり端折っていますが、実装の流れを備忘録で残します。
Draggable
のgithub
はこちらにあります。
環境
ruby 3.1.0
Rails 7.0.4
vue@3.2.41
インストール
著者はimportmap
を使用しているので以下のコマンドでpin
します。
そうすると、importmap.js
に自動書き込みが走ります。
./bin/importmap pin vuedraggable@next --download
使い方
その後、draggable
を使用可能にするために読み込み、
コンポーネントとして使用できるようにします。
import Draggable from "vuedraggable";
// 略
components: {
Draggable,
},
使い方は簡単で以下のように、ドラッグさせたいコンポーネントを <draggable>
で
挟むことで簡単にドラッグ&ドロップが実装できます。
vue3系からは <template #item>
をdraggable
タグの中に仕込まないと
エラーが発生するので注意が必要です。
// 略
<draggable>
<template #item="{ element }">
<!-- ★ここにドラッグ&ドロップさせたいコンポーネントを挿入します -->
</template>
</draggable>
<script>
import Draggable from "vuedraggable";
components: {
draggable,
},
}
今回は以下のように v-for
構文でCards
を1個ずつ表示している状態で、
card
同士を自由にドラッグできるようにすることをゴールにします。
<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
が出ます。
<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
を使います。
これは、ドラッグ&ドロップが終わったタイミングで発火します。
ちなみにイベントはこちらのページで綺麗にまとめられていました。
ありがてぇ
<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
マイグレーションファイルは以下のような感じ
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
送信します。
<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に保存することができました。
# 略
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
カラムの順番で画面に表示されるようになります。
#省略
def index
cards = Card.all.order(sort: :asc) # ここを修正
render json: cards, methods: :language
end
#省略
これで、ドラッグ&ドロップした情報がDBに保存され、リロードにも対応できるようになりました。
ドラッグ中のアニメーションなど置いといて、最低限の機能が実装できました。
最後に
ここまで読んでくださりありがとうございました。
実装方法はかなり簡単で、イベントも豊富なのでかなり気合いの入ったプラグインだと感じました。
サクッとドラッグ&ドロップを作りたいときに便利ですね。
参考
▼アニメーションをつけるなら
ドラッグ時の掴める箇所やアニメーションを設定