はじめに
データの並び順を自由自在に入れ替えたい、という要求があったとき、ドラッグアンドドロップで順番が変えられるとなかなか便利です。
そこで本記事ではドラッグアンドドロップによる表示順変更機能をRails 4で実装する手順を説明します。
補足資料
理解しやすくするための補足資料をいろいろ用意してみました。
デモサイト
Herokuにデモサイトを作ってみました。
元々のサンプルアプリケーションはCRUDができるようになっていますが、このデモサイトではデータの更新はできません。(変更できるのは順番のみ)
ドラッグアンドドロップするとどうなるか、実際に動かしてみてください。
スクリーンキャスト
サンプルアプリケーションを作る過程を録画してみました。
フルスクラッチでrails newするところから始めています。
http://www.youtube.com/watch?v=YQ-6HkBhVyc&feature=youtu.be
トータルで35分ぐらいあるのでソート機能を実装するところから見たい人は、9分40秒あたりから再生してください。
あと、画質も上げた方が字が潰れなくて見やすいはずです。
ソースコード
サンプルアプリケーションのソースコードはGitHubにアップしています。
こちらも参考にしてみてください。
参考文献
本記事はこちらのweb記事をアレンジして作成しました。
サンプルアプリケーションの仕様
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
注:バージョンによって参照パスが異なります
- jquery-ui-rails 5.x
//= require jquery-ui/sortable
(@md1961@githubさんのコメントを参照) - jquery-ui-rails 6.x
//= require jquery-ui/widgets/sortable
(Docs: Mention how the widget files moved in 6.0.0)
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のセットアップ方法から丁寧に説明しています。
良かったらこちらも読んでみてください。