テーブル内の行のドラッグ＆ドロップ操作でデータを追加する(Rails+HTML+JavaScript)

# はじめに
HTMLのdraggable属性をRailsと組み合わせたサンプルを作成したのでメモとして残しておきます。

# 参考にしたサイト
サンプルの作成にあたって下記のサイトを参考にさせていただきました。

[意外と知らないHTML5 API \- Drag & Drop APIとは \| CodeGrid](https://app.codegrid.net/entry/dnd-api-1)
[\[HTML5\] Drag & Drop API おさらい 「DOM 要素の DnD」 ｜ Developers\.IO](http://dev.classmethod.jp/references/html5-drag-drop-api-review-dom/)
[jQuery内からRailsのActionを叩く \- Qiita](http://qiita.com/somewhatgood@github/items/113773747a6faa800366)


# やりたかったこと

テーブル内の行をドラッグ＆ドロップすることで、別のテーブルにデータが追加される

![draggable1.png](https://qiita-image-store.s3.amazonaws.com/0/141934/e94bbb3c-a128-34ee-5f12-6857912c2136.png)　→　![draggable2.png](https://qiita-image-store.s3.amazonaws.com/0/141934/3b49fb95-2a4a-294c-d898-08a5040db2d8.png)

# テーブル

**Userテーブル**

| カラム名 | 型 | 説明 | 
| --- | --- | --- |
| name | string | ユーザ名 | 

スクリーンショット撮影時のデータ

| id | name | 
| --- | --- | --- |
| 1 | Yamada Taro |
| 2 | Yamada Jiro |
| 3 | Yamada Saburo |

**SelectedUserテーブル**

| カラム名 | 型 | 説明 |
| --- | --- | --- |
| user_id | integer |  UserIDを格納する |

スクリーンショット撮影時のデータ
操作前

| id | user_id | 
| --- | --- | --- |
| 1 | 1 |

操作後

| id | user_id | 
| --- | --- | --- |
| 1 | 1 |
| 2 | 2 |

# 準備

jqueryを利用できる状態にする

```ruby:Gemfile
gem 'jquery-rails'
```

```ruby:app/assets/javascripts/application.js
//= require jquery
```

Sampleコントローラを作成する

```shell:command
$ rails g controller sample index add
```

```ruby:routes.rb
get 'sample/index'
post 'sample/add'
```

テーブルを作成する

```shell:command
rails g scaffold User name:string
rails g scaffold SelectedUser user_id:integer
rake db:migrate
```

# Controller

データを一覧表示するView用のメソッドとデータ追加するメソッドを作成

```ruby:app/controllers/sample_controller.rb
class SampleController < ApplicationController

  protect_from_forgery except: [:add]

  def index
    @users = User.all
    @selected_users = SelectedUser.all
  end

  def add
    selected_user = SelectedUser.new
    selected_user.user_id = params[:id]

    if selected_user.save
      #head 201
      user = User.find(selected_user.user_id)
      hash = {id: user.id, name: user.name}
      require 'json'
      render :json => hash.to_json
    else
      head 500
    end

  end
end
```

# View

ドラッグ＆ドロップ部分の実装は[CodeGridさん](https://app.codegrid.net/entry/dnd-api-1)のサイトに掲載されているサンプルに少しだけ処理を加えたものになっております。

※ドラッグ＆ドロップ後の処理がおそまつですが、そこはおいおい修正することに・・・

```html+erb:app/views/sample/index.html.erb
<h1>Draggable Sample</h1>

<hr>

<!-- SelectedUser一覧をリスト表示 -->
<h2>SelectedUser</h2>

<table border="1" width="300">
  <tr>
    <th>id</th>
    <th>name</th>
  </tr>
  <% Array(@selected_users).each do |selected_user| %>
      <tr>
        <td><%= selected_user.user_id %></td>
        <td><%= User.find(selected_user.user_id).name %></td>
      </tr>
  <% end %>
  <tr>
    <!-- ドロップエリア -->
    <td colspan="3" id="dropzone" class="dropzone"></td>
  </tr>
</table>

<br>

<!-- User一覧をリスト表示 -->
<h2>User</h2>

<table id="list" border="1" width="300">
  <tr>
    <th>id</th>
    <th>name</th>
  </tr>
  <% Array(@users).each do |user| %>
      <!-- rowにドラッグ可能(draggable)属性を指定 -->
      <tr class="item" draggable="true" id="<%= user.id %>">
        <td><%= user.id %></td>
        <td><%= user.name %></td>
      </tr>
  <% end %>
</table>


<script>

    $(function () {

        // dropzoneの表示テキストを初期化
        initDropzone();

        // listテーブルのitem行が操作された時のリスナーを設定
        items = document.getElementById('list').getElementsByClassName('item');

        Array.prototype.forEach.call(items, function (item) {
            $(item).on('dragstart', onDragStart);
            $(item).on('dragend', onDragEnd);
        });

        // dropzoneのリスナーを設定
        var $dropzone = $('#dropzone')
            .on('dragover', onDragOver)
            .on('dragenter', onDragEnter)
            .on('dragleave', onDragLeave)
            .on('drop', onDrop);


        // dropzoneの表示テキストを指定
        function initDropzone() {
            $('#dropzone').text("ここにドロップできます。");
        }

        function startDropzone() {
            $('#dropzone').text("ドラッグ中。");
        }

        function endDropzone(name) {
            $('#dropzone').text(name + "をドロップしました。");
        }

        // ドロップ時の処理
        // (1) ドロップされた行のidをPOSTする
        // (2) 成功したらリダイレクトする
        // (3) 失敗したらダイアログを表示する
        function doAction(id) {
            $.ajax({
                url: "<%=  sample_add_path  %>",
                type: "POST",
                data: {
                    id: id
                },
                dataType: "html",
                success: function (data) {
                    //alert("success");

                    // dataにドラッグ＆ドロップした
                    // Userのid, nameがjson形式で
                    // 渡される
                    // console.log(data);
                    // {"id":1,"name":"Yamada Taro"}

                    // 暫定的にページを再読込
                    location.href = "<%= sample_index_path %>"
                },
                error: function (data) {
                    alert("errror");
                }
            });
        }

        // ドラッグ＆ドロップ操作
        function onDragStart(e) {
            var id = e.originalEvent.target.id;
            var name = e.originalEvent.target.cells[1].innerHTML;
            e.originalEvent.dataTransfer.setData('id', id);
            e.originalEvent.dataTransfer.setData('name', name);
            addDraggingEffect();
            startDropzone();
        }

        function onDragEnter(e) {
            addEnterEffect();
        }

        function onDragLeave(e) {
            removeEnterEffect();
        }

        function onDragOver(e) {
            e.preventDefault();
        }

        function onDragEnd(e) {
            resetAllEffect();
        }

        function onDrop(e) {
            e.preventDefault();
            var id = e.originalEvent.dataTransfer.getData('id');
            var name = e.originalEvent.dataTransfer.getData('name');
            endDropzone(name);
            doAction(id);
        }

        function addDraggingEffect() {
            $dropzone.addClass('dragging');
        }

        function removeDraggingEffect() {
            $dropzone.removeClass('dragging');
            initDropzone();
        }

        function addEnterEffect() {
            $dropzone.addClass('dragenter');
        }

        function removeEnterEffect() {
            $dropzone.removeClass('dragenter');
        }

        function resetAllEffect(e) {
            removeDraggingEffect();
            removeEnterEffect();
        }

    });

</script>


<style>

  .dropzone p {
    margin: 0;
    padding: 0;
    background-color: #0000E9;
  }

  .dropzone.small {
    height: 40px;
  }

  .dropzone {
    border: 1px solid #555;
    margin-top: 5px;
    /*width: 246px;*/
    min-height: 100px;
    font-size: 12px;

    text-align: center;
    line-height: 100px;

    color: gray;
    background-color: ghostwhite;
    border-color: gray;
  }

  .dragging {
    color: rgba(0, 0, 0, .4);
    background-color: rgba(0, 0, 255, .1);
    border-color: rgba(0, 0, 255, .3);
  }

  .dragging:after {
  }

  .dragenter {
    border-color: rgba(255, 0, 0, .3);
    background-color: rgba(255, 0, 0, .1);
  }

</style>
```