Edited at

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

More than 1 year has passed since last update.


はじめに

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

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


補足資料

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


デモサイト

Herokuにデモサイトを作ってみました。

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

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

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


スクリーンキャスト

サンプルアプリケーションを作る過程を録画してみました。

フルスクラッチでrails newするところから始めています。

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

トータルで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ですし、何もしなくても構いません。


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

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

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

table_sort.css.scss

+.table-sortable {

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


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

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

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でドラッグアンドドロップ機能をテストする方法(スクリーンキャスト付き)