【スマホ対応】ドラッグ＆ドロップで連番の並び替えをする。「acts_as_list」+「Sortable.js」 

# ドラッグ＆ドロップで自動連番の処理を実装する

### イメージ
[![Image from Gyazo](https://i.gyazo.com/16b9256b293b1a1d3fa813c720bb1d1b.gif)](https://gyazo.com/16b9256b293b1a1d3fa813c720bb1d1b)

### 実装経緯
- 並び替え機能はメニュー表など管理画面で実装頻度が高かった。
- 管理画面もスマホやタブレットに対応してほしいという声も多かった。

実装以前は、編集フォームに遷移して順番(`position`)カラムの整数値をセレクトフィールドで選択して更新していました。
また、自動連番処理はモデル側でしてました。

知識を整理するため簡単なアプリ上に作ってみようと思います。

### 使用するライブラリ
- [acts_as_list](https://github.com/brendon/acts_as_list)
順番の並び替えの制御を担当する。
同ポジションにパフォーマンスが上の[ranked-model](https://github.com/mixonic/ranked-model)がある。
今回`acts_as_list`を使用した経緯
  - レコード数が少ない
  - 今後もレコード数は多くはならない
  - 少ないレコード数なので順番の値を把握しておきたい。
  **※レコード数が多く順番の値を気にしない場合は[ranked-model](https://github.com/mixonic/ranked-model)を選択した方がよさげ**

- [SortableJS](https://github.com/SortableJS/Sortable)
ドラッグ＆ドロップで要素の並び替えを担当する
今回`SortableJS`を使用した経緯
  - jQueryUIを使わずに実装したかった。(jQuery UIのドラッグ操作がスマホ対応していないのが難点)
  - スマホ特有のドラッグ操作を`handle`で制御出来る

### 実装

##### 1. 順番(`position`)カラムの追加
今回は`Menu`モデルに`position`カラムを追加する形で作っていきます

```ruby
class AddPositionToMenu < ActiveRecord::Migration[6.1]
   def change
     add_column :menus, :position, :integer, null: false, default: 0
     add_index :menus, :position # ここちょっと怖い
   end
 end
```

※ ここでindexに`unique`を設定すると`acts_as_list`の仕様で順番を更新できなくなります。


##### 2. acts_as_listを追加

   ``` ruby
    gem "acts_as_list"
   ``` 
   `bundle install`する

- 対象のモデルに`acts_as_list`を記述する

```ruby
class Menu < ApplicationRecord
   acts_as_list
   validates :title, presence: true, length: { maximum: 50 }		   validates :title, presence: true, length: { maximum: 50 }
```

##### 3. jqueryを追加

- ajax処理が書きやすい（慣れている）ため導入します

  `yarn add jquery`

- jqueryの初期化
`config/webpack/environment.js`

```js
const { environment } = require('@rails/webpacker')
// jqueryの設定
const webpack = require('webpack')
environment.plugins.prepend('Provide',
    new webpack.ProvidePlugin({
        $: 'jquery/src/jquery',
        jQuery: 'jquery/src/jquery'
    })
)
module.exports = environment

```

##### 4. SortableJsの追加

- 導入
  `yarn add sortablejs`

- view設定
  id: `js-sortable-menus`内をソートの範囲
  class: `i.handle`をドラッグ&ドロップのトリガーにする
  `app/views/menus/index.html.haml`

```haml
  %tbody#js-sortable-menus
    - @menus.each do |menu|
      %tr
        %td
          %i.handle
           .fa.fa-list-ul
        %td= menu.title
```

  - sortablejsの設定
    `app/javascript/packs/views/menus/index.js `

```js
import Sortable from 'sortablejs';

$(function() {
    const el = document.getElementById('js-sortable-menus');
    new Sortable(el, {
        handle: "i.handle",
        axis: 'y',
        animation: 300,
        onUpdate: function (evt) {
            return $.ajax({
                url: `/api/menu/positions/${evt.oldIndex}`,
                type: 'patch',
                data: {
                    from: evt.oldIndex,
                    to: evt.newIndex
                }
            });
        }
    });
});

```

`handle:`でトリガーになる要素を設定
`oldIndex`は移動前の画面上の順番(0から始まる)
`newIndex`は移動後の画面上の順番(0から始まる)

- api作成
  まずnamespaceを使ってapiのroutesを記述する
  ` config/routes.rb`

```ruby
Rails.application.routes.draw do
  root to: "static_pages#index"
  resources :menus

  namespace :api do
    namespace :menu do
      resources :positions, only: [:update]
    end
  end
end
```

 - apiコントローラーの作成 
 `app/controllers/api/menu/positions_controller.rb`

```ruby
class Api::Menu::PositionsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def update
    menu = Menu.find_by!(position: params[:from].to_i + 1) # 0から始まるので+1する
    menu.update!(position: params[:to].to_i + 1) # 0から始まるので+1する
    head :ok
  end
end

```

` skip_before_action :verify_authenticity_token`で`CSRF トークン`の検証をスキップする

##### 5. 順番(position)カラム順に出力する


`app/models/menu.rb`

```ruby
scope :sorted, -> { order(position: :asc) }
```

`app/controllers/menus_controller.rb`

```ruby

  # GET /menus or /menus.json
  def index
    @menus = Menu.sorted
  end

```

`app/views/menus/index.html.haml`

順番を並び替えるviewに`js`ファイルを読み込ませる

```haml
= javascript_pack_tag "views/menus/index", "data-turbolinks-track": "reload"
%h1 Listing menus

%table
  %thead
    %tr
      %th
      %th Title
      %th Price
      %th
      %th
      %th

  %tbody#js-sortable-menus
    - @menus.each do |menu|
      %tr
        %td
          %i.handle
            .fa.fa-list-ul
        %td= menu.title
        %td= menu.price
        %td= link_to "Show", menu
        %td= link_to "Edit", edit_menu_path(menu)
        %td= link_to "Destroy", menu, method: :delete, data: { confirm: "Are you sure?" }

%br

= link_to "New Menu", new_menu_path
```




### 成果
[Demo動作確認](http://wagai-recipe.herokuapp.com/menus)
※Herokuの無料プランなので立ち上がりに時間がかかる時があります。


[![Image from Gyazo](https://i.gyazo.com/16b9256b293b1a1d3fa813c720bb1d1b.gif)](https://gyazo.com/16b9256b293b1a1d3fa813c720bb1d1b)
[Github](https://github.com/wagai/demo/pull/16)


### 参考にさせて頂いた記事
- [ドラッグ＆ドロップで並び替えて順番を保存できるリストを作る](https://qiita.com/at-946/items/28cad21211b9305d6077)
- [jQueryUIを使わずにドラッグ＆ドロップを実装したい](https://qiita.com/AVELWP/items/536c797a63c081cf7bc0)
- [ドラッグ操作をスマホにも対応させるTIPS](https://blog.ruco.la/memo/250)
- [**伊藤さんの記事**ドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)](https://qiita.com/jnchito/items/391fb16d3f69fda9bdae)

### 

[自社の公開記事から](https://www.webya-wagai.jp/articles/1)