はじめに
Todoアプリなどでよくある「ドラッグ&ドロップ(以下D&D)」機能、憧れますよね。
自分もアプリ開発の際に実装してみたくなり、挑戦しました。
ただ、大きな壁にぶつかりました・・・
更新するとせっかく並び替えた順番が元に戻る
ほとんどこの一言につきます。
そして、ドラッグ&ドロップの実装をまとめてくださっている記事は、
自分が調べた限り、フロントサイドで話が完結しているものがほとんどでした。
もしくはローカルストレージを用いたもの。
私の場合フロントはNuxt.jsを用いてVue.jsで開発し、
サーバーサイドはRailsのAPIモードを使用しておりました。
そのため、何とかデータベースに「D&Dした後の順番」を保存したかったのです。
今回は上記の環境で「ブラウザの更新をしても順番を保持してくれる」D&Dの実装をまとめます。
対象
- D&D機能を実装した上でローカルストレージではなく、データベースに状態を保存したい方
- ある程度Vue.jsやNuxt.jsに理解がある方
→私の理解が甘く、説明を端折ってる部分があるため「あー多分こういう事が言いたいのか」と補完できる方がオススメです。
ただ、自分と同じ初学者の方向けに、0から私がここにたどり着いた流れも最後に載せますので、新しくチャレンジしたい方も一読していただけたら幸いです。
##目次
目次 |
---|
できるようになることと記事の流れ |
前半戦「保存できないD&Dリストを作る」 |
後半戦「更新しても順番をキープしたいんじゃ! |
非同期でページ遷移して戻ってきたら、順番が戻ってしまう方へ |
おまけ |
##前提
Nuxt.jsとRailsAPIモードのプロジェクトの立ち上げが完了し、axios&APIから送られてきたjsonでデータのやり取りができている状態でお話を進めます。
RailsAPIはlocalhost:5000
、Nuxtはlocalhost:3000
で向き先を設定しています。
蛇足ですが、仮想環境での設定の仕方も記事にしておりますので、わからない方はこちらをご覧ください。
仮想環境でRails API × Nuxt.jsのアプリケーション開発をしたいが、まずブラウザに表示されない!
※言い訳ですがプログラミングの学習を始めて3ヶ月目、Vue.jsは3週間の初学者です。
言い回しの間違い等あるかと思いますが、ご容赦くださいませ。
また、後学のため是非ご指摘をお願いします。
本当に大変だったので、要所要所で自分の思いが漏れています。
無駄な吐露ですが「わかるわかる!」となってもらえたら嬉しいです。
それでは始めて行きましょう!!!!!!!!!
##できるようになることと記事の流れ
できること:更新しても順番が崩れないD&Dリストが作れます。
記事の前半は、Vue.jsでD&Dを実装する際に必要なものや、その実装の方法をご紹介し、更新しても順番は保持できないが、D&Dはできるリストの作成を行います。
記事の後半では、前半で作ったリストにAPIへリクエストする機能を実装させるための方法をお伝えし、更新しても順番が保持できるようにしていきます。
そして最後に、より良いD&Dリストを作るために気づいたことや、参考にさせていただいたリンクのご紹介をさせていただきます。
それでは、よろしくお願いします。
前半戦「保存できないD&Dリストを作る」
早速、必要なものをご紹介します。
今回、私が使用したのは「Vue.Draggable」というコンポーネントプラグインです。
こういう便利なものがあることは知っていましたが、「コンポーネントプラグイン」という総称がついていたことは初めて知りました。
~~詳しい方には怒られてしまうかもしれませんが、~~あまり難しく考えず**「ドラッグできるタグを作ってくれるやつ」**とここでは覚えておいてください。
公式のgithub:SortableJS/Vue.Draggable
上のgithubをみても正直よく分からなかったので、まずVue.DraggableをNuxt.jsで使う方法を調べました。
結論から申し上げますと、一番参考になったページはこちらです。
「どーやってNuxt.jsでVue.Draggableを使うんじゃい!」と困っている外国の方の叫びが見れます。私と全く同じ状況でした。Vue.jsでVue.Draggableを使っている記事は多いのですが、まずNuxt.jsで使用している記事にたどり着くまでに時間がかかりました。
Nuxt.jsのファイル設定
コンポーネントの関係性など私と異なる部分もあるかと思いますが、自分は以下の設定で動きました。
1.Vue.Draggableをインストール
npm install vue-draggable
もしくは、yarn add vue-draggable
でプロジェクトにインストールしましょう。これで使う準備ができました。
2.pluginフォルダ内にvue-draggable.jsを作成
プロジェクトの直下にpluginフォルダがあるはずです。そこに新しく「vue-draggable.js」という名前でファイルを作り、以下を記載してください。
import Draggable from 'vuedraggable';
import Vue from 'vue';
Vue.component('draggable', Draggable);
// Draggableという機能をdraggableという名前で呼べるようにしている
// export default draggableは不要
3.nuxt.config.jsに追記
こちらもプロジェクト直下にあるファイルです。ぱーっと下げていくと以下の箇所があると思うので、追記してください。
//省略
plugins: [
"@/plugins/vue-draggable" //これを追記してください。
],
//省略
4.コンポーネントなどの実際のvueファイルにタグを記載して試す
私の場合、子コンポーネントに当たるTodoList.vueというファイルで試し、成功しました。
その時の記述はこちらです。(無関係な場所は省略しています。)
<ul>
<draggable v-model="todos" :options="{ animation: 200, delay: 50 }" >
<li v-for="todo in todos" :key="todo">
{{ todo.title }}
</li>
</draggable>
</ul>
上で紹介した外国の方のissueをみると、彼は以下のような記載をしっかりしていますが、私はどちらも記載せず動きました・・・。ここは自分の開発の状況が関係している可能性が高いので、動かない場合は、以下の記述を追記した方が良いと思います。
※Draggableなんてタグは変だよ!みたいなエラーが出た場合。
<script>
import draggable from 'vue-draggable'
export default {
components: {
draggable
},
//省略
</script>
export default draggable //追記
一応、上のコードについて自分の状況を説明します。
todosは親から渡されているTODO(タスクです)のタイトルやid等の情報が入っています。
もちろん、propsにも"todos"が記載されています。
liタグにv-forを記載し、v-for="todo in todos"
と書くことで、todosの中身を{{ todo }}の中に出力し、また1つずつリストを作ってくれます。 今回はタイトルを出力させています。
アプリケーションを立ち上げ、確認すると
- 1番目のTODOです
- 2番目のTODOです
- 3番目のTODOです
といった感じでリストアップされていると思います。
このリストの好きな項目をD&Dすると見事、順番が入れ替わると思います!
番外編:落とし穴
自分がここで「動かない!」と落とし穴にハマったので、一応記載します。
当初、v-model="todos"
をDraggableのタグに記載し忘れていました。
何が起こるかというと、ドラッグ機能がfalseに自動的に設定されるので、動かなくなります。
これはChromeの検証をしてliタグを見た結果わかりました。
HTMLを直接書き換えてtrueにしたところちゃんと動きました。
このことから、最初はdataの方にdraggable: true
なんて初期値を設定していましたが、v-modelを書くだけで解決したので、皆さんも是非そのようにしてください。
#後半戦 「更新しても順番をキープしたいんじゃ!」
当時の私の状況
「更新すると元に戻る。関連記事も見つからない。」
ざっとこの時の悩みは以下の点です
- 入れ替えた情報をどのように保存するのか
- そもそもどの段階でイベントが発生して、リクエストを飛ばすのか
- 「入れ替えた後」って何のカラムに何を保存するのか
- ユーザーにカラムを持たせるの?TODOにカラムを持たせるの?それとも独立?
- Gemを使って何かする方が良いのか(あれ?APIとしての役割ではない?)
- 更新して順番が変わらず、かつ他の画面に遷移した後で、非同期で戻っても値が保たれるか?
非常に辛かったです。
全部解決できます!一緒に頑張っていきましょう!!
考え方の共有
混乱していた原因は、最終的に何が行われるのかが全くわからず、不安だったためです。敢えて今回は動くコードよりも先に、「更新しても順番を保つことができる」理屈と考え方をお伝えします。
少し長くなりますが、モヤモヤしている人は絶対にスッキリするので是非読んでください。
1.「順番は呼び出す際に決めるもの、データベース内で並べるわけではない。」
当初、私は**「並べ替えたいものに関し(今回はTODOですね)データベース内で「id」のように決まり切った順番が設定されるべきだ。」**と考えていました。
ただ、TODOを削除したら間が抜けてしまうし、じゃあidを元に順番を決めるかと言われればデータベース内できっちり順番が決まっているだろうから難しいと思いました。そもそもidの書き換えとか怖い。
そんな時に、こんなアドバイスをいただきました。
「順番は呼び出す際に決めるもの、データベース内で並べるわけではない。」
どんなにデータベース内でユーザーに紐づくTODOの順番が乱れていても、呼び出す際に綺麗な順番にしてあげられればフロント側に反映できるということに気づきました!
つまり、**「TODOの順番が変わった時に毎回変わる番号を記録し、呼び出す時にその番号を使う」**必要があることが推測できました。
生徒(TODO)たちは絶対に変わることのない名簿番号(id)とは別に、グラウンド(ブラウザ)での並び方を決めるために整列番号(sort)を持ちます。このsortはaxios先生が生徒の順番を変えるたびに新たに決められ、生徒たちはデータベース学校からグラウンドに呼び出されるたび、その順番で綺麗に並びます。
ちなみに先生のセリフの最中がまさにD&Dしているって感じです。
このイメージを持ったまま次に進んでください。
2.Railsでsortカラムの追加を行う
では、設定するべきカラムは何で、何のモデルに設定するかはわかりましたね?
そう、並べ替えたいモデルに対して、integer型、defaultを0でsortカラムを追加してください。
$ rails g migration Addカラム名Toテーブル名 カラム名:データ型
私の場合は、
$ rails g migration AddSortToTodos sort:integer
default値をコマンドでどう設定するかはわからなかったので、大人しくマイグレーションファイルに追記してdb:migrateを実行しました。分かる方は鼻で笑ってコメントでやり方を教えてください。
このsortカラムを使って順番を定義し、呼び出す際にはこの順番で並んで貰うというわけです。
データベース内で並ぶというよりも、呼び出される時に並ぶということを忘れずに。
これで、問題点の
- 入れ替えた情報をどのように保存するのか
- 「入れ替えた後」って何のカラムに何を保存するのか
- ユーザーにカラムを持たせるの?TODOにカラムを持たせるの?それとも独立?
- Gemを使って何かする方が良いのか(あれ?APIとしての役割ではない?)
ここは解決できましたね。答えは上から、
- 「sort」という可変の数値に呼び出す順番として保存する
- TODO(並び替えたいもの)と紐づくsortカラムに順番(数値)を保存する
- TODOに持たせる
- Gemは今回は要らない
です。では、次はフロント側に戻って、どのように記述してAPI側に順番を保存させるか決めましょう。ただ、APIのルーティングやコントローラーを設定するので行ったり来たりします。頑張りましょう!
axios先生の出番です。
3.axiosでapiを叩きに行く
フロント側からRails側にaxiosを使ってAPIを叩く必要性はわかりました。
では、いつ叩くか?今でしょ
学校のイメージを見て分かる通り、先生のセリフが終わったタイミングで整列順番が確定しています。
すなわち「並べ替え終わった後」
もっとわかりやすく言えばD&Dが終わったタイミングです。
では、具体的にどう設定するかを述べます。
Vue.Draggableはありがたいことに発行しているイベントが多いです。
今回は、その中のendを使います。これは、D&Dが終わったタイミングで発火します。
<ul>
<draggable v-model="todos" :options="{ animation: 200, delay: 50 }" @end="atEnd">
<!-- @endを追加し、D&Dが終わったタイミングでatEndに発火するようにしました -->
<li v-for="todo in todos" :key="todo">
{{ todo.title }}
</li>
</draggable>
</ul>
<!-- 省略 -->
<script>
//省略
async atEnd() {
let result =
await axios.patch(`v1/todos`, {
todo: this.todos
});
const updateUser = {
...this.user,
todos: this.todos
};
this.$store.commit("setUser", updateUser);
},
console.log(result);
// 値がしっかり返ってきているか確認用
//省略
</script>
ざっくりとコードの説明をします。
D&Dが終わったタイミングで、patchのHTTPリクエストをAPIに投げます。この際、パラメーターには全てのTODOのsortを更新させるため、todoにtodosを全て入れています。updateUserは現在のユーザーの情報をstoreに保存し、さらに呼び出すために使用しています。あとでコード詳細を貼りますね。
詳しくは、こちらの記事をご覧ください。
[入門]Rails API × Nuxt SPA × Firebase Authで作る Todo Appチュートリアル
では、patchリクエストに対応するコントローラーのアクションも見ていきましょう。
一応ですが、APIのconfig/route.rbにpatch 'todos', to: 'todos#sort'
みたいな感じで書くのをお忘れなく。ここは各々の設定があると思います。
# 省略
def sort
params[:todo].each_with_index do |t,i|
@todo = Todo.find(t[:id])
@todo.update( sort: i )
end
render json: {result: "ok"}
#ここがちゃんと機能すれば、consoleでokが表示されます。
end
# 省略
では、コードの説明をします。
まず、params[:todo]にはユーザーのtodosが全て詰まっています。binding.pryなどを用いて是非目で確認してください。これを、.each_with_indexを用いて、順番に出力していきます。
ここで重要なのは、@todo = Todo.find(t[:id])
の部分です。何をしているかと言うと、idを元に、Todoのモデルからこのユーザーが持っているTodoを全て取得し、変数に入れているのです。
では、**何のためにそうするのか?**と言うと、取得した全てのsortカラムに「i(index)」・・・すなわち0からTODOの数までindexを振って保存させるためです。
例えば、TODOが5個あればsortカラムには0から4の数字が入り、updateで更新されます。
※.each_with_indexの使い方に関してはこちらの方の記事をご覧ください。
each_with_indexの使い方
しかも、送った順番のままeachで排出され、sortカラムにindexが振られていきます。
ここはよーく考えて、是非理解してから先に進んでください。
【具体例】
[id:1,title:勉強する,sort:0]
[id:2,title:手を洗う,sort:1]
[id:3,title:筋トレする,sort:2]
↓この状態で「勉強する」と「手を洗う」を入れ替えた場合
[id:2,title:手を洗う,sort:0]
[id:1,title:勉強する,sort:1]
[id:3,title:筋トレする,sort:2]
こうなります。送られてきた順番でidが検索され、そこにsortが1つずつ入っていくのです。
これで、API側に順序を登録することはできました!
- そもそもどの段階でイベントが発生して、リクエストを飛ばすのか
これは解決できましたね。
D&Dのイベントが終わったタイミングです。
sortをupdateするのでpatchでリクエストしてください。
全体でいうところの3/4くらいが終わりました!もう少しです。
余談ですが、リストにクラスをつけて、background-colorとmarginを設定すると見やすいかもしれません。また、ここまでの段階でよくわからない事があれば是非コメントまでお願いします。
では、後少し、頑張っていきましょう!
3.リストがあるページにアクセスする際、jsonを渡せば順番を保つことができる
またここから、各々の状況によるかと思いますが「登録したsortの順番で呼び出す」方法をお伝えします。
結論を述べますと、D&Dを行うページで並び替えたものを呼び出す際、コントローラー側でsortの順で呼び出してjsonを返せば良いのです。
私の場合、TodoList.vueというD&Dできるリストはpages/user.vueで呼び出して使っているので、API側ではusers_controller.rbの#indexが対応することになっています。
その記述を見ていきましょう。
#省略
def index
if params[:uid]
user = User.find_by(uid: params[:uid])
todos = user.todos.order(point: "ASC")
render json: {user: user, todos: todos}
else #ここから下はあまり気にしないでください。
@users = User.all
render json: @users
end
end
#省略
大切なのはこの二行です。
todos = user.todos.order(sort: "ASC")
render json: {user: user, todos: todos}
一行目でユーザーに紐づくtodo全てを、sortカラムを参照して昇順で変数に入れています。
D&Dをし終わった際に初めて、TODO達は次に呼ばれる時の番号を持たされるので、その順番で呼び出してあげるというわけです。
お気づきの方もいると思いますが、だからこそsortは初期値0で問題がないのです。
D&Dをした際に自動的に割り振られるような構造ですもんね。
二行目で、userの情報と分けてtodosの順番を保ったままjsonで返しています。
これで、D&Dをした順番でTODOをフロント側に送る事ができます!
あとは、各々の状況に合わせてフロント側の表示を調整してください。
ちょっと難しいぞ?という方は、console.logを使ってAPI側から返されるjsonを見ながら表示をさせてみてください。
うまくいけば、これで保存できるD&Dリストが完成しているはずです!お疲れ様でした!!
##非同期でページ遷移して戻ってきたら、順番が戻ってしまう方へ
自分に最後に起きた問題は、更新しても順番を保つことのできるD&Dリストができたものの、逆に非同期で他のページに行って戻ってくると順番が元に戻り、更新するとちゃんとD&Dした順番になるという現象でした。。。これでは良くないですね。
こちらも各々の状況によって解決方法が異なると思うので一律のことは言えませんが、最後に自分の関係各所コードの状況を共有して締め括りとしたいと思います。皆様それぞれ異なる開発環境や状況だと思いますが、ご参考になれば幸いです。
1.関係各所のコード
関係各所のコードを掲載します。
基本的にこちらの記事を参考にしています。
[入門]Rails API × Nuxt SPA × Firebase Authで作る Todo Appチュートリアル
・ドラッグ&ドロップができるリストが表示されるpageはこちら
<templete>
<TodoList :todos="user.todos" />
</templete>
<!-- 色々省略しています -->
<script>
// 省略
computed: {
user() {
return this.$store.state.currentUser;
}
},
// 省略
</script>
・実際のコンポーネント
<ul>
<draggable v-model="todos" :options="{ animation: 200, delay: 50 }" @end="atEnd">
<!-- @endを追加し、D&Dが終わったタイミングでatEndに発火するようにしました -->
<li v-for="todo in todos" :key="todo">
{{ todo.title }}
</li>
</draggable>
</ul>
<!-- 省略 -->
<script>
//省略
async atEnd() {
let result =
await axios.patch(`v1/todos`, {
todo: this.todos // D&D実装にあたり追加しました。重要!
});
const updateUser = {
...this.user,
todos: this.todos // D&D実装にあたり追加しました。重要!
};
this.$store.commit("setUser", updateUser);
},
console.log(result);
// 値がしっかり返ってきているか確認用
//省略
</script>
こんな感じです。私も完全に理解しているわけではないので、ざっくり予想も含め説明すると、D&Dが終わったタイミングでatEndが動き、帰ってきたjsonの情報でupdateUserがされることで、ユーザーのtodoの順番がsort順になるのだと思われます。
最後に、Railsのcurrent_user的なものを擬似的に作っていますので、その記述を見てみましょう。
components/user.vueにおけるtodos: this.todos
の送り先です。
2.ユーザーに紐づくtodoを設定、かつcurrent_userとして反映している場所
import firebase from "@/plugins/firebase"
import axios from "@/plugins/axios"
const authCheck = ({
store,
redirect
}) => {
firebase.auth().onAuthStateChanged(async user => {
if (user) {
const {
data
} = await axios.get(`/v1/users?uid=${user.uid}`)
store.commit("setUser", { ...data.user, todos: data.todos }) //重要
} else {
store.commit("setUser", null)
}
});
}
export default authCheck
todoを追加する際に、storeにsetUserとして情報を渡しています。uidと言うのがfirebaseを利用してAPI側でユーザーを特定している記述なのですが、その際にdata.userにスプレッドオペレータを用いてtodos: data.todos
を含めた配列を作り直しています。
・最後にmutationとstateとコントローラーです
const mutations = {
setUser(state, payload) {
state.currentUser = payload
},
//省略
const state = {
currentUser: {
user: {
experience_point: 0
},
todos: [], //ここが記事と関係のある部分になります。
rewards: [],
untilPercentage: null,
untilLevel: null,
},
loading: false,
notification: {
status: false,
message: ""
},
errors: []
}
export default state
def index
if params[:uid]
user = User.find_by(uid: params[:uid]) #関係のある部分です
if !user.present?
render json: {}
else
todos = user.todos.order(sort: "ASC") #関係のある部分です
rewards = user.rewards.order(sort: "ASC") #関係のある部分です
totalExp = user.experience_point
user_level = CalcUserLevel.calc_user_level(user, totalExp)
render json: {user: user, todos: todos, rewards: rewards, untilPercentage: user_level[:until_percentage], untilLevel: user_level[:until_level]}
end
else
@users = User.all
render json: @users
end
end
この記述により、自分の場合は画面遷移をしても順番をキープする事ができました。
ここまでお付き合いいただき、ありがとうございました!!お疲れ様でした!!
おまけ
D&D機能をせっかく実装したのに、「D&D出来る」とユーザーに伝える方法に悩んでしまいました。
単純ですが、自分が感動したので共有させてください!
・マウスカーソルを合わせた時にカーソルを手のポインタに変える
これだけで「あ、持ち上げて移動できるんだ」とユーザーに伝わりやすくなりました。
方法は、cssでliタグ(もしくはD&Dさせるもの)に対し、cursor: grab;
と記述してあげるだけです。自分はhover時だけの適用になりましたが、きっとマウスを押している間は開いた手を閉じるポインターにする・・・など、色んなことができそうですね。
フロントエンド開発の奥深さを知りました。
参考サイト紹介と、Vue.jsの初学者の方向けに自分の学習フローなど共有
3月の下旬にVue.jsに挑戦すると決めた時に、一番やってみたかったのは保存のできるD&D機能でした。正直RailsをAPIモードとして使うのも、Nuxt.jsを触るのも初めてだったので、何とかここまで来れて良かったです。
支えてくださった現役エンジニアの皆様と、素晴らしい記事のおかげです。順番にご紹介します。
[入門]Rails API × Nuxt SPA × Firebase Authで作る Todo Appチュートリアル
Vue.jsもRailsAPIもNuxtも何もわからないまま挑戦して、こんなに難しいんだ・・・でもすごい!とびっくりした始まりの記事です。この記事に挑戦し、分からないところを調べ、再度挑戦し・・・を繰り返しています。「自分で説明できないところを0にする」事が目標ですが、なかなか難しいです。
今後Vue.jsに挑戦する方には是非オススメしたい記事です。
掌田 津耶乃『Vue.js&Nuxt.js超入門』
(一般のAmazonのリンクです)何故か奇跡的に、上の記事と同様、Nuxt+firebase+Vue.jsで開発する応用編も掲載されています。この本を勉強しつつ上のチュートリアルに挑戦するのが一番いいと思います。こっちが先でもいいかもしれない。ただ、私もまだペーペーなので、他のVue.jsに精通している方にしっかり質問をした上で選んでください。
Vue.jsでドラッグアンドドロップによる要素の並べ替えと移動を実装する
挑戦したい!と思ったきっかけのうちの一つの記事と、ここにはVue.Draggableの提供してくれるイベントがまとめてあります。
Nuxt.JS プラグイン(公式)
今回はVue.Draggableを使いましたが、プラグインの入れ方が載っているので参考にしてください。
Vue.js リストレンダリング(公式)
D&D機能はおそらくv-forと併用するはずです。自分はここの理解も甘かったので、一度公式に目を通してからスタートした方が良いかもしれません。
Use with Nuxt?
記事で紹介したNuxt.jsにVue.Draggableを導入しようとしてうまくいかなかった時のやりとりがまとめられている公式githubのissueです。友達になりたいです。
https://qiita.com/yoshiplum
https://twitter.com/codeplumdev
こちらの実装にあたり、詰まったところに暖かくアドバイスをくださった現役エンジニアの方です。本当にありがとうございました。
改めて、この機能実装にあたりご協力をしてくださった皆様に感謝申し上げます。
来月から未経験ながら就活が始まりますので、応援をよろしくお願いします。
以上です!!!!!!!!!!!