ドラッグ&ドロップで並び替え【Rails】【jQuery】

# この記事の内容

TODOリストを共有できるアプリを作っていて、自分のTODO一覧をドラッグ&ドロップで並び替えができる機能を実装しました。
（コンセプトは「人生でやりたいこと100のリストの共有」なので、todoをdreamという言葉を使って表現しています。）

基本的には[こちら【Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)】](https://qiita.com/jnchito/items/391fb16d3f69fda9bdae)の記事にしたがって実装していきました。
上記記事はRails4なので、Rails5で変わったところや、自分なりに理解に時間がかかったところなどに適宜説明を入れながら書いてみようと思います。


# 前提
Rails 5.2.3
jQuery 3.4.1

# 構成
userの詳細ページ(show)にdreamの一覧が表示されています。
viewの構成としては、
`/views/users/show.html.erb`内で同じ階層の`_dream.html.erb`が部分テンプレートとして呼ばれ、userのdreamを繰り返し表示しています。

CSSフレームワークはMaterializeを使用しています。

アソシエーション(今回関連するもののみ)：
users - has_many :dreams
dreams - belongs_to :user

# 流れ
1. ranked-modelの導入
2. jquery-ui-railsの導入
3. ルーティング
4. Controller
5. Viewにカスタムデータ属性を追加
6. jQuery

## 1. ranked-modelの導入
ranked-modelは、`row_order`というカラムでレコードをソートし、並び替えた時の全体の再配置やリバランスを自動的に行ってくれるgemです。

```:Gemfile
gem 'ranked-model'
```
`bundle install`も忘れずに。

次に、migrationファイルを作成して、
dreamsテーブルに`row_order`カラムをinteger型で追加します。
ここに並び順を表す数字が入ります。

そしてmodelです。

```ruby:app/models/dream.rb
include RankedModel 
ranks :row_order , with_same: :user_id 
```
これによりrankメソッドが使えるようになります。
今回はuserごとにグルーピングして並び順をつけたいので、`with_same: :user_id`を追加します。

これでControllerに以下のように書くことでrow_order順に一覧表示されます。

```ruby:app/controllers/user_controller.rb
def show
  @dreams = @user.dreams.rank(:row_order)
end
```

## 2. jquery-ui-railsの導入
そもそもjQueryUIとは、jQueryをベースにしたJavaScriptのライブラリです。
jquery-ui-railsはそれをrailsに導入するためのgemです。

```:Gemfile
gem 'jquery-ui-rails'
```
`bundle install`も忘れずに。

jQueryUIのうち今回使うものをapplication.jsに読み込みます。

```js:app/javascripts/application.js
//= require jquery3
//= require rails-ujs
//= require jquery-ui/widgets/sortable
//= require jquery-ui/effects/effect-highlight
```
`//= require jquery-ui`とすると全てを読み込めますが、重くなってしまうので必要なものだけがいいと思います。
この記述の仕方については[GitHub](https://github.com/jquery-ui-rails/jquery-ui-rails)に載っています。

## 3. ルーティング

```ruby:config/routes.rb
resources :dreams do 
  put :sort
end
```
これにより、`PUT /dreams/:dream_id/sort`で`dreams#sort`が呼ばれます。

## 4. Controller
並べ替えが行われた時に実行されるsortアクションを定義します。

```ruby:app/controller/dreams_controller.rb
def sort
  dream = Dream.find(params[:dream_id])
  dream.update(dream_params)
  render body: nil 
end

private
def dream_params
  params.require(:dream).permit(:content, :opened, :status, :row_order_position) 
end
```
`render body: nil`はアクション実行後にViewをレンダリングしたくない時に使います。Rails5からそれまでより少し記述が変わったようです。

`dream_params`の中身の`row_order_position`について、カラム名は`row_order`なんですが、`_position`をつけないといけません。これはgemの仕様みたいです。

## 5. Viewにカスタムデータ属性を追加
jQueryでajaxを行う際に必要になるので、カスタムデータ属性を設定しておきます。

```erb:app/views/users/_dream.html.erb
<%= content_tag "tr", id: "dream-#{dream.id}", class: "item", data: { model_name: dream.class.name.underscore, update_url: dream_sort_path(dream)} do %>
```
(当初は普通に\<tr>タグの中に変数を<%= %>で埋め込んで実行してみたのですが、どこかで間違えたのかエラーになってしまったので、`content_tag`を使っています。)

この`data-model_name`と`data-update_url`が次の項目で`modelName`、`updateUrl`となって出てきます。

`dream.class.name.underscore`ですが、
`class`と`name`はrubyのメソッドで、それぞれオブジェクトのクラスとその名前を取得します。
`underscore`はrailsのメソッドで、クラス名をファイル名に変換します。
(例:「Product.underscore」→「product」、「AdminUser.underscore」→「admin_user」)

## 6. jQuery
並べ替えの実装をするtable_sort.jsを作成します。


```js:app/javascripts/table_sort.js
$(function(){
  $('#dreams_list').sortable({ ★１
    update: function(e, ui){ ★２
      let item = ui.item; ★３
      let item_data = item.data(); ★４
      let params = {_method: 'put'}; ★５
      params[item_data.modelName] = { row_order_position: item.index() } ★６
      $.ajax({
        type: 'POST',
        url: item_data.updateUrl, ★７
        dataType: 'json',
        data: params ★８
      });
    },
    stop: function(e, ui){ ★９
      ui.item.children('td').not('.item__status').effect('highlight', { color: "#FFFFCC" }, 500)
    }
  });
});
```
★1
jQueryUIのsortableを使います。
sortableメソッドは、デフォルトで「ターゲット要素直下の子要素全て」を並べ替えの対象とするので、今回は並べ替え対象となるlistの親要素となっている`#dreams_list`を選択しました。
もし「子要素全て」以外を並べ替え対象とするなら、updateの前に`items: ''`でその要素を指定します。

★2
updateパラメーターにはドラッグで並び順が変更されたタイミングで呼び出されるイベントを指定します。
第1引数はイベントオブジェクト、第2引数には、以下のプロパティを持ったオブジェクトです。
> <img width="663" alt="スクリーンショット 2019-07-17 17.07.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427185/7c951b33-cfed-ca83-4490-4e7341ae78ff.png">
http://stacktrace.jp/jquery/ui/interaction/sortable.html

★3
上記uiで取得したデータのうちのitemを変数itemに代入します。

★4
上記で定義したitemのdata属性をitem_dataに代入します。
itemの中にもたくさんのデータが入っています。
その中のdatasetというところに入っているようです。
`console.log(item)`で確認したところdatasetの内容は以下のようになっていました。
> dataset: DOMStringMap
　modelName: "dream"
　updateUrl: "/dreams/58/sort"

ここで、Viewで設定したdata属性が返ってきているのがわかります。

★5
後ほどajaxでdataとして送るparamsを定義します。
まずは`_method`を使い、PUTだよという情報を入れてあげます。
（指定しないとGETかPOSTしか送れないようです。まだきちんと調べられていません…）

★6
先ほどのparamsに追加で情報を入れます。
`params[item_data.modelName]`は、★4で確認したように`dream`が入ります。
`{row_order_position: item.index()}`は`index()`メソッドを使って、その要素がjQueryオブジェクトの何番目か(0から数えて)を取得します。
つまり、paramsに`dream: {row_order_position: 0(例)}`というようなデータが渡されます。

★7
★4で確認したように、`/dreams/1(例)/sort`のような形になります。

★8
結果としてparamsの中身は`{_method: ‘put’, dream: {row_order_position: 0(例)}}`となります。

★9
stopパラメーターは並び替えが終了したときに呼び出されます。
今回は`item__status`クラス以外にハイライトが当たるようにしています。

### 補足
当初ドラッグ&ドロップはできるが、その結果が保存されずリロードすると元に戻るという状態でした。
要因は、rails-ujsを読み込んでいなかったことです。

`//= require rails-ujs`については[こちらの記事](https://qiita.com/ichikawa-hiroki/items/f5df892ff85afea51b8c)を参考にしました。

読み込む順番に注意。

```js:app/javascripts/application.js(正)
//= require jquery3
//= require rails-ujs
//= require jquery-ui/widgets/sortable
//= require jquery-ui/effects/effect-highlight
//= require_tree .
```


## 完成！
**並び替えが実装できました！**

完成版のコードは[こちら](https://github.com/fumikao/dreams-100)をご参照ください。

分かりにくい点や間違い等ありましたらご指摘お願いいたします。



#### 参考
https://qiita.com/jnchito/items/391fb16d3f69fda9bdae
http://stacktrace.jp/jquery/ui/interaction/sortable.html
https://qiita.com/ichikawa-hiroki/items/f5df892ff85afea51b8c