【Rails】ドラッグ&ドロップ機能の実装(エフェクト付き)

# 目標

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/579893/644abec1-5b50-5865-618f-942344d9631d.gif)

# 開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

# 前提

下記実装済み。

・[Slim導入](https://qiita.com/matsubishi5/items/b639ffddecc626856039)
・[Bootstrap3導入](https://qiita.com/matsubishi5/items/b117c95f4dd71340cc80)
・[投稿機能実装](https://qiita.com/matsubishi5/items/1a5aeab651b5822d1f2c)

# 実装

### 1.Gemを導入

```ruby:Gemfile
# 追記
gem 'jquery-ui-rails'
gem 'ranked-model'
```

`jquery-ui-rails`
➡︎ jQuery UIを使用出来るようにする。

`gem 'ranked-model'`
➡︎ Bookの並び順を管理出来るようにする。

### 3.カラムを追加

```terminal:ターミナル
$ rails g migration AddRowOrderToBooks row_order:integer
```

```terminal:ターミナル
$ bundle
```

```ruby:schema.rb
create_table "books", force: :cascade do |t|
  t.integer "user_id"
  t.string "title"
  t.text "body"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.integer "row_order"
end
```

### 4.モデルを編集

Bookモデルで`ranked-model`を使えるようにする。

```ruby:book.rb
# 追記
include RankedModel
ranks :row_order
```

### 5.コントローラーを編集

**①`indexアクション`を編集**

```ruby:books_controller.rb
# 変更前
@books = Book.all

# 変更後
@books = Book.rank(:row_order)
```

**②ストロングパラメーターに`row_order_position`を追加する。**

カラム名は`row_order`だが、ここではGemの使用上`row_order_position`と記述する。

```ruby:books_controller.rb
def book_params
  params.require(:book).permit(:title, :body, :row_order_position)
end
```

**③`sortアクション`を追加**

```ruby:books_controller.rb
def sort
  book = Book.find(params[:book_id])
  book.update(book_params)
  render body: nil
end
```

`render body: nil`
➡︎ アクションだけを実行し、ビューは呼び出さない。

### 6.ルーティングを編集

```ruby:route.rb
# 変更前
resources :books

# 変更後
resources :books do
  put :sort
end
```

### 7.ビューを編集

```slim:books/index.html.slim
/ 「table-sortable」というクラスを付与
table.table.table-bordered.table-sortable
  thead
    tr
      th
        | 投稿者
      th
        | タイトル
      th
        | 本文
      th

  tbody
    - @books.each do |book|
      / trタグにクラスを付与し、データを設定する
      tr.item(data = { model_name: book.class.name.underscore, update_url: book_sort_path(book) })
        td
          = link_to book.user  do
            = book.user.name
        td
          = link_to book.title, book_path(book)
        td
          = book.body
        td
          -if book.user == current_user
            = link_to '削除', book, method: :delete, data: { confirm: '本当に削除してもよろしいですか？' }, class: 'btn-sm btn-danger'
```

`model_name: book.class.name.underscore`
➡︎ 「book」はeach分のブロック変数にあたる。

### 8.`application.js`を編集

```javascript:application.js
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require jquery
//= require jquery-ui/widgets/sortable // 追記
//= require jquery-ui/effects/effect-highlight // 追記
//= require bootstrap-sprockets
//= require_tree .
```

`//= require jquery-ui/widgets/sortable`
➡︎ jQuery UIを有効化

`//= require jquery-ui/effects/effect-highlight`
➡︎ ドラッグ&ドロップ時のエフェクトを有効化

### 9.JavaScriptファイルを作成・編集

```terminal:ターミナル
$ touch app/assets/javascripts/table_sort.js
```

```javascript:table_sort.js
$(function(){
  $('.table-sortable').sortable({
    axis: 'y',
    items: '.item',

    // Ajaxで並び順のデータをコントローラーに送信
    update(e, ui) {
      let item = ui.item;
      let item_data = item.data();
      let params = { _method: 'put' };
      params[item_data.model_name] = { row_order_position: item.index() };
      $.ajax({
        type: 'POST',
        url: item_data.update_url,
        dataType: 'json',
        data: params,
      });
    },

    // ドラッグ幅をテーブルに合わせる
    start(e, ui) {
      let cells, tableWidth, widthForEachCell;
      tableWidth = $(this).width();
      cells = ui.item.children('td');
      widthForEachCell = tableWidth / cells.length + 'px';
      return cells.css('width', widthForEachCell);
    },

    // エフェクトを付与
    stop(e, ui) {
      return ui.item.children('td').effect('highlight');
    },
  });
});
```

### 10.カーソルのデザインを変更

```scss:application.scss
// 追記
.table-sortable {
  tr.item {
    cursor: row-resize;
  }
}
```