Rails

Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)

More than 1 year has passed since last update.

はじめに

データの並び順を自由自在に入れ替えたい、という要求があったとき、ドラッグアンドドロップで順番が変えられるとなかなか便利です。

そこで本記事ではドラッグアンドドロップによる表示順変更機能をRails 4で実装する手順を説明します。

補足資料

理解しやすくするための補足資料をいろいろ用意してみました。

デモサイト

Herokuにデモサイトを作ってみました。
元々のサンプルアプリケーションはCRUDができるようになっていますが、このデモサイトではデータの更新はできません。(変更できるのは順番のみ)

ドラッグアンドドロップするとどうなるか、実際に動かしてみてください。

http://sortable-table-sandbox.herokuapp.com/

sample-app.gif

スクリーンキャスト

サンプルアプリケーションを作る過程を録画してみました。
フルスクラッチでrails newするところから始めています。

http://www.youtube.com/watch?v=YQ-6HkBhVyc&feature=youtu.be

Screen Shot 2014-07-31 at 7.57.58.png

トータルで35分ぐらいあるのでソート機能を実装するところから見たい人は、9分40秒あたりから再生してください。

https://www.youtube.com/watch?v=YQ-6HkBhVyc&feature=youtu.be&t=9m40s

あと、画質も上げた方が字が潰れなくて見やすいはずです。

ソースコード

サンプルアプリケーションのソースコードはGitHubにアップしています。
こちらも参考にしてみてください。

https://github.com/JunichiIto/sortable-table-sandbox/tree/1st-implementation

参考文献

本記事はこちらのweb記事をアレンジして作成しました。

http://benw.me/posts/sortable-bootstrap-tables/

サンプルアプリケーションの仕様

Scaffoldで作った果物管理アプリを使います。
果物管理アプリといってもなんてことはない、単に果物の名前を入力できる単純なアプリケーションです。

ちなみに、表示順変更機能を実装する前のテーブルのマークアップはこんな感じになっています。(viewはSlimを使っています)

fruits/index.html.slim

table.table.table-bordered
  thead
    tr
      th Name
      th
      th
      th
  tbody
    - @fruits.each do |fruit|
      tr
        td = fruit.name
        td = link_to 'Show', fruit
        td = link_to 'Edit', edit_fruit_path(fruit)
        td = link_to 'Destroy', fruit, data: {:confirm => 'Are you sure?'}, :method => :delete

ドラッグアンドドロップで表示順を変更できる機能を実装する

ここから実装の手順を説明します。

1. ranked-model gemで順番を管理できるようにする

Railsで順番を管理するgemとして、ranked-model gemを使います。
acts_as_listも有名ですが、ranked-modelの方がパフォーマンス的に優れています。

以下の手順でranked-modelをインストールしてください。

Gemfile

+gem 'ranked-model'
$ bundle install

Fruitモデルの並び順をranked-modelで管理できるようにします。

fruit.rb

 class Fruit < ActiveRecord::Base
+  include RankedModel
+  ranks :row_order
 end
$ rails g migration AddRowOrderToFruits row_order:integer
$ rake db:migrate

fruits_controller.rb

  def index
-  @fruits = Fruit.all
+  @fruits = Fruit.rank(:row_order)
 end

上のコードのように、row_order順に並び替える場合はrankメソッドを使います。

2. jQuery UIをインストールする

ドラッグアンドドロップを実現するためにjQuery UIを使います。
jQuery UIのインストールには jquery-ui-rails gemを使うと手軽です。

Gemfile

+gem 'jquery-ui-rails'
$ bundle install

application.js

+//= require jquery.ui.sortable

注:バージョンによって参照パスが異なります

3. sort アクションを用意する

Ajaxでデータを更新できるように、sort アクションを用意します。

routes.rb

 Rails.application.routes.draw do
-  resources :fruits
+  resources :fruits do
+    put :sort
+  end

   root to: 'fruits#index'
 end

fruits_controller.rb

+# this action will be called via ajax
+def sort
+  fruit = Fruit.find(params[:fruit_id])
+  fruit.update(fruit_params)
+  render nothing: true
+end

 private

   # Never trust parameters from the scary internet, only allow the white list through.
   def fruit_params
-    params.require(:fruit).permit(:name)
+    params.require(:fruit).permit(:name, :row_order_position)
   end

sort アクションではリクエストされたレコードの row_order を更新します。
ただし、実際には row_order ではなく、row_order_position という属性を更新している点に注意してください。(ranked-model gemの仕様です)

4. JavaScriptでAjaxリクエストを送信する

次に、JavaScriptでAjaxリクエストを送信できるようにします。
ここでは table_sort.js.coffee という新しいファイルを作成しました。

table_sort.js.coffee

+$ ->
+  $('.table-sortable').sortable
+    axis: 'y'
+    items: '.item'
+
+    update: (e, ui) ->
+      item = ui.item
+      item_data = item.data()
+      params = { _method: 'put' }
+      params[item_data.modelName] = { row_order_position: item.index() }
+      $.ajax
+        type: 'POST'
+        url: item_data.updateUrl
+        dataType: 'json'
+        data: params

View側もJSにあわせてdata属性やCSSクラスを追加します。

fruits/index.html.slim

 h1 Listing fruits

-table.table.table-bordered
+table.table.table-bordered.table-sortable
   thead
     tr
       th Name
       th
       th
       th
   tbody
     - @fruits.each do |fruit|
-      tr
+      tr.item(data = { model_name: fruit.class.name.underscore, update_url: fruit_sort_path(fruit) })
         td = fruit.name
         td = link_to 'Show', fruit
         td = link_to 'Edit', edit_fruit_path(fruit)

5. 動作確認する

これでドラッグアンドドロップ操作が行えるようになっているはずです。
画面を動かしてみてください。

問題なく動いていれば画面をリロードしてもドラッグアンドドロップしたときの順番が保持されているはずです。

うまく動かないときはログを開いてエラーが出ていないか確認してください。
正常に動いていればこんな感じのログが出ているはずです。

development.log

Started PUT "/fruits/4/sort" for 127.0.0.1 at 2014-07-31 06:33:27 +0900
Processing by FruitsController#sort as JSON
  Parameters: {"fruit"=>{"row_order_position"=>"1"}, "fruit_id"=>"4"}
  Fruit Load (0.1ms)  SELECT  "fruits".* FROM "fruits"  WHERE "fruits"."id" = ? LIMIT 1  [["id", 4]]
   (0.1ms)  begin transaction
  Fruit Load (1.1ms)  SELECT  "fruits"."id", "fruits"."row_order" FROM "fruits"  WHERE ("fruits"."id" != 4)  ORDER BY "fruits"."row_order" ASC LIMIT 2 OFFSET 0
  Fruit Load (0.1ms)  SELECT  "fruits"."id", "fruits"."row_order" FROM "fruits"  WHERE ("fruits"."id" != 4) AND "fruits"."row_order" = 1376257  ORDER BY "fruits"."id" ASC LIMIT 1
  SQL (1.2ms)  UPDATE "fruits" SET "row_order" = ?, "updated_at" = ? WHERE "fruits"."id" = 4  [["row_order", 1376257], ["updated_at", "2014-07-30 21:33:27.996058"]]
   (0.7ms)  commit transaction
  Rendered text template (0.0ms)
Completed 200 OK in 23ms (Views: 1.9ms | ActiveRecord: 3.4ms)

6. Viewをお化粧する

機能的にはこれでもOKなのですが、少しだけ見栄えを良くしてみましょう。
ここは好みなので、好きなように変更してもOKですし、何もしなくても構いません。

カーソルのタイプを変更する

テーブルの上にカーソルが当たったときにカーソルのタイプを変えて、ソートできることをアピールします。

Screen Shot 2014-07-31 at 8.11.18.png

ここでは table_sort.css.scss というファイルを追加しました。

table_sort.css.scss

+.table-sortable {
+  tr.item {
+    cursor: row-resize;
+  }
+}

ドロップ時にハイライトさせる

ドロップしたときに行全体を黄色くハイライトさせます。

highlight2.gif

application.js

+//= require jquery.ui.effect-highlight

table_sort.js.coffee

         url: item_data.updateUrl
         dataType: 'json'
         data: params
+    stop: (e, ui) ->
+      ui.item.children('td').effect('highlight')

ドラッグ中の幅をテーブルに合わせる

そのままだとドラッグ中の行の幅が縮んでしまうので、テーブルの幅に合わせます。

table_sort.js.coffee

         url: item_data.updateUrl
         dataType: 'json'
         data: params
+    start: (e, ui) ->
+      tableWidth = $(this).width()
+      cells = ui.item.children('td')
+      widthForEachCell = tableWidth / cells.length + 'px'
+      cells.css('width', widthForEachCell)
     stop: (e, ui) ->
       ui.item.children('td').effect('highlight')

7. Turbolinks特有の問題を回避する

Rails 4で新規にアプリケーションを作ると、デフォルトでTurbolinksが有効になっています。
そのため、他の画面(showやedit)に進んでからindex画面に戻ってくるとドラッグアンドドロップができなくなります。(table_sort.jsが呼び出されない)

この問題を回避するために、jquery-turbolinks gemを導入します。

Gemfile

+gem 'jquery-turbolinks'
$ bundle install

application.jsでturbolinksを一番最後にrequireするのがポイントです。

application.js

 //= require jquery
+//= require jquery.turbolinks
 //= require jquery_ujs
 //= require jquery.ui.sortable
 //= require jquery.ui.effect-highlight
-//= require turbolinks
 //= require bootstrap
 //= require_tree .
+//= require turbolinks

補足: 既存レコードがある場合

既存レコードがある場合は何らかのルールでrow_orderカラムを更新しておいた方が良いと思います。

以下はid順にrow_orderカラムを更新する際のサンプルスクリプトです。

ActiveRecord::Base.record_timestamps = false
Fruit.transaction do
  Fruit.order(id: :desc).each do |fruit|
    fruit.update_attribute :row_order_position, 0
  end
end
ActiveRecord::Base.record_timestamps = true

まとめ

実現するまでに若干のステップを踏まなくてはなりませんが、gemやjQueryをうまく組み合わせると意外と簡単に実現できます。
同じようなUIを作成する必要が出てきたら、ぜひ参考にしてみてください!

2014.08.02追記: テストコードを書く方法も公開しました!

RSpecを使ってこのドラッグアンドドロップ機能をテストする方法を説明してみました。
こちらもスクリーンキャスト付きです。RSpecのセットアップ方法から丁寧に説明しています。

良かったらこちらも読んでみてください。

初心者大歓迎!RSpec 3でドラッグアンドドロップ機能をテストする方法(スクリーンキャスト付き)