#147 Sortable Lists (revised)
- Download:
- source codeProject Files in Zip (86.4 KB)
- mp4Full Size H.264 Video (18.3 MB)
- m4vSmaller H.264 Video (7.55 MB)
- webmFull Size VP8 Video (10.9 MB)
- ogvFull Size Theora Video (15.8 MB)
下の画面はFAQ(よくある質問)のリストを表示しているサイトのページです。リストの項目をドラッグ&ドロップで並べ替えて、ページをリロードせずに更新された並び順をデータベースに反映させる機能を付加しようと思います。以前のエピソードでは、この機能をPrototypeとScriptaculousのSortableヘルパーを用いて実現しました。しかし、これらは現在のRailsには含まれていません。Rails 3.1でこれを実現するにはどうすればいいでしょうか?
jQuery UIを導入する
Prototypeの代わりに今回はjQuery UIを使用します。この中に含まれるSortableプラグインを使うことによって、簡単にドラッグ&ドロップで並べ替えができるリストを簡単に作ることができます。このプラグインは、たくさんのオプションで設定を変更することができ、アプリケーションに合うように振る舞いをカスタマイズできます。
RailsアプリケーションにjQuery UIを追加するのは簡単です。何もダウンロードする必要はありません。ただapplication.js
ファイルに1行追加するだけです。
// This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of // the compiled file. // //= require jquery //= require jquery-ui //= require jquery_ujs //= require_tree .
ビューを修正する
FAQリストのテンプレートは次のようになっています。
<h1>FAQs</h1> <ul> <% @faqs.each do |faq| %> <li> <%= link_to h(faq.question), faq %> </li> <% end %> </ul> <p><%= link_to "New FAQ", new_faq_path %></p>
リストにidを付与し、JavaScriptから参照できるようにします。リスト中の項目ごとにもidを付け、項目を動かすことによって更新された並び順をサーバに送り返したときにそれがわかるようにします。個々の項目は<model_name>_<model_id>の形式でidを持つ必要があります。content_tag_forというヘルパーメソッドを使ってこれらを自動的に生成します。これらの修正を行ったテンプレートは以下のようになります。
<h1>FAQs</h1> <ul id="faqs"> <% @faqs.each do |faq| %> <%= content_tag_for :li, faq do %> <%= link_to h(faq.question), faq %> <% end %> <% end %> </ul> <p><%= link_to "New FAQ", new_faq_path %></p>
内容を出力するため、content_tag_for
ブロックの始めのタグにイコール記号が入っていることに留意してください。
リストにソート機能を付加する
次にリストをソート可能にするのに必要なスクリプトを追加します。ただリストを取得してsortable()
を呼び出すだけです。このコードをjQuery
関数でラップ(wrap)して、ページのDOMがロードされたときだけ実行されるようにします。
jQuery ->; $('#faqs').sortable();
ここでページをリロードすると、リスト内の項目をドラッグできるようになります。
sortableに対して多くのオプションを渡すことができます。axis
オプションを使うことでリストが縦方向にのみソートできるようにし、項目がドロップされた時に起動されるupdate
コールバックを付加します。
jQuery -> $('#faqs').sortable( axis: 'y' update: -> alert('updated!') );
ここで項目をドラッグしてみると縦方向のみに動き、ドロップするとコールバックが起動してアラートが表示されます。
更新された並び順を保存する
項目がドロップされたらサーバに何らかの情報を送り、新しい並び順を保存できるようにしなければなりません。現在のアプリケーションのままでは、ページをリロードするとデフォルトの状態に戻ってしまいます。
各項目の位置を保存するためにFaq
モデルにposition
フィールドが必要です。この変更のためのマイグレーションを作成し、rake db:migrate
を実行してデータベースに新しいフィールドを追加します。
$ rails g migration add_position_to_faqs position:integer
次にFaqsController
のindex
アクションを書き換えて、FAQをpositionフィールドの順番で取得するように修正します。
def index @faqs = Faq.order("position") end
FAQの並び順が変わったときにサーバに送り返されたデータを処理するためのアクションを新たに作成しなくてはいけません。FaqsController
は標準の7つのRESTfulなアクションを持っていますが、いずれもここでは役に立ちません。あえて言うとupdate
アクションが求めるものに近いですが、一度に複数の項目を更新する必要があります。コントローラにsort
アクションを追加する予定ですが、今はただnothing
と表示するだけにします。
def sort render nothing: true end
このカスタムで作成したアクションを、routesファイルのfaqs
リソースの項に追加します。sort
アクションは複数のFaq
に対して作用するので、sort
のためにcollection
とpost
を使用します。ここでは本当ならput
を使うべきなのですが、より扱いやすいpost
を使うことにします。
Faqapp::Application.routes.draw do root to: 'faqs#index' resources :faqs do collection { post :sort } end end
CoffeeScriptファイルに戻って、コールバック関数のコードを修正して、alert
を表示する代わりに新しいsort
アクションに対してPOSTリクエストを行うようにします。CoffeeScriptの中に直接URLを入れない方がいいので、リストに新しいdata
属性としてコールバックURLを追加します。
<ul id="faqs" data-update-url="<%= sort_faqs_url %>">
CoffeeScriptコードでこのURLを取得し、これを使って更新された位置の情報をsort
アクションに送り返します。$(this).sortable('serialize')
を呼び出すことでこの情報を取得することができます。これですべての情報をサーバに送り返せる形式でまとめることができました。
jQuery -> $('#faqs').sortable( axis: 'y' update: -> $.post($(this).data('update-url'), $(this).sortable('serialize')) )
ここでページをリロードして、項目の位置を変えると、sort
アクションへのAJAX呼び出しが起動されます。開発(development)ログを見てみると、パラメータがfaq
のパラメータとしてfaq
のid
の配列とともに渡される様子がわかります。これらは更新されたリストの並び通りに送られます。
Started POST "/faqs/sort" for 127.0.0.1 at 2011-10-17 22:05:47 +0100 Processing by FaqsController#sort as */* Parameters: {"faq"=>["1", "2", "5", "3", "4", "6"]} Rendered text template (0.0ms) Completed 200 OK in 21ms (Views: 20.2ms | ActiveRecord: 0.0ms)
FaqsController
のsort
アクションでこれらのパラメータを読んで、index
で項目をループして各Faq
の位置を更新します。
def sort params[:faq].each_with_index do |id, index| Faq.update_all({position: index+1}, {id: id}) end render nothing: true end
update_all
を用いてすべての更新を1回のクエリで実行し、各Faq
の位置をindex + 1
に設定します(indexはゼロ始まりなので)。ここでid
はパラメータのidです。リストでFAQの位置を変更してページをリロードすると項目はその位置にとどまります。
ハンドルを追加する
現状ではページ上の項目をドラッグできるということは見た目からはわかりません。これを改善するために、項目がドラッグ可能であることをユーザに伝えるために各項目にハンドルを追加します。テンプレートで各項目にハンドルを追加してそれにclass
を設定し、CoffeeScriptから参照できるようにします。
<ul id="faqs" data-update-url="<%= sort_faqs_url %>"> <% @faqs.each do |faq| %> <%= content_tag_for :li, faq do %> <span class="handle">[drag]</span> <%= link_to h(faq.question), faq %> <% end %> <% end %> </ul>
あと必要なのは、sortable
関数でhandle
オプションを指定することです。
<p>ハンドルにスタイルも追加し、<code>move</code>カーソルを設定してドラッグ可能であることを示します。</p> ``` /app/assets/stylesheets/faqs.css.scss #faqs .handle { font-size: 12px; color: #777; cursor: move; }
ページをリロードすると、各項目の横にハンドルが表示されそれを使って項目をドラッグすることができます。
新規項目を追加する
新しくFAQを追加するとposition
属性はnull
になります。新規項目のposition
に自動的にリストの最後に来るように値が与えられるのがいいでしょう。
これはacts_at_list
gemを利用することで可能になります。このgemが公開されてからかなり経ちますが今でもSwanand Pagnisによって活発にメンテナンスされているので、問題なく利用できます。このgemをアプリケーションに追加するには通常と同じようにGemfile
にgemを追加してbundle
を実行してインストールします。
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not" required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'acts_as_list'
acts_as_list
をインストールしたら、Faq
モデルに追加します。
class Faq < ActiveRecord::Base acts_as_list end
新規に作成するFaq
のposition
属性が自動的に設定されるようになりました。