ドラッグアンドドロップで並べ替え(Rails + Ajax)

Ruby(とRails)を担当している石原です。
ソーシャル「OSを入れた後にインストールする10のアプリケーション」(仮) を作る過程をレポートしています。
これまでのエントリーはこちら ↓
- つくるぶガイドブログ: Ruby on Rails を使ってひとりでサービスを作ってみよう
- つくるぶガイドブログ: ひとりサービスの雛型をつくる(リキッドレイアウト、GetText、Acts as Authenticated)
- つくるぶガイドブログ: Rails で楽々ソーシャルブックマークの仕組みを作る
- つくるぶガイドブログ: Rails プラグイン acts_as_taggable_redux でタグクラウドを作ろう
今回は、登録した10のアプリケーションをベスト1からベスト10まで並べ替えたい、ということで、ちょっと趣向を凝らしてそれをドラッグアンドドロップで出来るようにしたいと思います。
Acts As List でブックマークをリスト化
登録したアプリケーションのブックマークを並べ替えられるようにするには、ブックマークに順序を持たせる必要があります。
Acts As List を使うと、モデルの各オブジェクトに順序を持たせ、順番を並べ替えることができるようになります。
まずは、Bookmark モデルに acts_as_list 宣言を追加します。
== app/models/bookmark.rb == class Bookmark < ActiveRecord::Base belongs_to :user belongs_to :software acts_as_list :scope => :user end
ブックマークの順序は各ユーザーごとに持たせるので、
:scope => :user
としています。
ブックマークを順位順に並べるために、User モデルを修正します。
== app/models/user.rb == class User < ActiveRecord::Base has_many :bookmarks, :order => :position
bookmarks テーブルに position というカラムを追加します。position という名前を付けることによって、acts_as_list はそれを自動的に順序として認識します。
== db/migrate/007_add_position_to_bookmarks.rb ==
class AddPositionToBookmarks < ActiveRecord::Migration
def self.up
add_column "bookmarks", "position", :integer
end
def self.down
remove_column "bookmarks", "position"
end
end
マイグレーションを実行し、DBを変更します。
rake db:migrate
Acts As List により、bookmark.move_higher あるいは bookmark.move_lower で順番を操作できるようになりました。
また、bookmark.first? あるいは bookmark.last? で、そのブックマークが最初に位置しているか、最後に位置しているかを判定できるようになります。
これらを利用し、以下のようにコントローラーとビューを修正して、リスト上で並べ替えできるようにします。
== app/controllers/bookmarks_controller.rb
class BookmarksController < ApplicationController
# position 順にリストする
def list
@bookmark_pages, @bookmarks = paginate(
:bookmarks,
:conditions => ['user_id = ?', current_user.id],
:order => 'position',
:per_page => 10
)
end
# 以下の2つのアクションを追加
def move_higher
Bookmark.find(params[:id]).move_higher
redirect_to :action => 'list'
end
def move_lower
Bookmark.find(params[:id]).move_lower
redirect_to :action => 'list'
end
end
== app/views/bookmarks/list.rhtml ==
途中略
<td><%= link_to 'Show', :action => 'show', :id => bookmark %></td>
<td><%= link_to 'Edit', :action => 'edit', :id => bookmark %></td>
<td><%= link_to 'Destroy', { :action => 'destroy', :id => bookmark },
:confirm => 'Are you sure?', :method => :post %></td>
<td><%= bookmark.first? ? '' : link_to('▲', :action => 'move_higher', :id => bookmark) %></td>
<td><%= bookmark.last? ? '' : link_to('▼', :action => 'move_lower', :id => bookmark) %></td>
下図、矢印マークをクリックすれば、順番を並べ替えられます。
AJAX 用ヘルパーを使ってドラッグアンドドロップを実装
矢印マークをクリックすることで順番を並べ替えられるようになりましたが、今回のゴールはドラッグアンドドロップでの並べ替えです。
Rails には、Ajax の処理を簡単におこなえるよう、Prototype と Script.aculo.us との連携を可能にしたヘルパーが様々用意されています。
ドラッグアンドドロップも、そのうちのひとつを使って簡単に実装することができます。
まずは、prototype.js など必要な Javascript ライブラリを読み込むよう設定します。
レイアウトファイルの head タグの中ならどこでも良いので、
== app/views/layouts/application.rhtml == <%= javascript_include_tag :defaults if params[:controller] == 'bookmarks' %>
を追加します。とりあえず、bookmarks コントローラーが呼び出されたときだけ読み込むようにしました。
次に、先ほど矢印マークを付け加えたブックマーク一覧を表示するビューの改造です。
== app/views/bookmarks/list.rhtml ==
途中略
<h1>Listing bookmarks</h1>
<ul id="sortable_bookmarks">
<%= render :partial => 'sortable_bookmarks', :locals => {:bookmarks => @bookmarks} %>
</ul>
<%= link_to 'Previous page', { :page => @bookmark_pages.current.previous } if @bookmark_pages.current.previous %>
<%= link_to 'Next page', { :page => @bookmark_pages.current.next } if @bookmark_pages.current.next %>
一覧を表示する部分を partial ビューに切り出します。
また、元々 table タグで書かれていたのを <ul> と <li> を使ってリストに変更しました。
ul タグの id を sortable_bookmarks としています。これは、後述するドラッグアンドドロップを可能にするヘルパーで使います。
切り出した partial ビューは以下の通りです。
== app/views/bookmarks/_sortable_bookmarks.rhtml ==
<% for bookmark in bookmarks %>
<li id="bookmark_<%= bookmark.id %>" class="draggable">
<b><%= bookmark.position %>
<%= link_to h(bookmark.software.title), :controller => 'softwares', :action => 'show', :id => bookmark.software.id%>
<%= link_to 'Show', :action => 'show', :id => bookmark %> |
<%= link_to 'Edit', :action => 'edit', :id => bookmark %> |
<%= link_to 'Destroy', { :action => 'destroy', :id => bookmark }, :confirm => 'Are you sure?', :method => :post %> |
<%= bookmark.first? ? '' : link_to('▲', :action => 'move_higher', :id => bookmark) %>
<%= bookmark.last? ? '' : link_to('▼', :action => 'move_lower', :id => bookmark) %>
</li>
<% end %>
<%= sortable_element 'sortable_bookmarks',
:url => {:controller => 'bookmarks', :action => 'sort'},
:update => 'sortable_bookmarks' %>
ここで重要な変更は、li に bookmark_[bookmark の ID] という形式の id を割り当てたこと、draggable というクラスを指定したことと、最後の sortable_element ヘルパーです。
sortable_element ヘルパーの最初の引数 'sortable_bookmarks' で、ドラッグアンドドラッグ可能なオブジェクトグループの id を指定しています。sortable_bookmarks は先ほど ul タグに指定した id です。
:url => ... でドロップされたときに XHR で呼び出される URLを指定しています。bookmarks コントローラーの sort アクションです。
:update => ... で XHR 呼び出しの結果更新される DOM 要素を指定しています。sortable_bookmarks が指定されていますから、更新されるのは ul の中身、つまり partial ビューの部分となります。
では、XHR で呼び出しを受ける sort アクションを定義しましょう。
とりあえず
== app/controllers/bookmarks_controller.rb
def sort
render :nothing => true
end
などとアクションの中身を空にしておきます。
ブックマークのリストを表示し、1番目の項目をドラッグして2番目の後ろにドロップしてみます。このとき、development.log をモニターしていれば、以下のようなリクエストが確認できるはずです。
Processing BookmarksController#sort (for 127.0.0.1 at 2008-01-09 03:54:42) [POST]
Session ID: 18fb9c59d4cedce8a7b24938f7252007
Parameters: {"sortable_bookmarks"=>["2", "1", "3", "4", "5", "6", "7", "8", "9", "10"],
"action"=>"sort", "controller"=>"bookmarks"}
Parameters のところで、sortable_bookmarks パラメーターとして数字の配列が渡されています。これは、li の id として指定した bookmark_[bookmark の ID] のアンダースコアよりも後ろの部分、つまり bookmark の ID をドラッグアンドドロップ直後の並び順で並べたものになっているのです。
sort アクションでは、ブックマークをこの配列の順番に並べ替え、position 値を新たに設定すればよいことになります。
== app/controllers/bookmarks_controller.rb
def sort
params[:sortable_bookmarks].each_with_index do |bookmark_id, i|
Bookmark.find(bookmark_id.to_i).update_attributes(:position => i + 1)
end
@bookmark_pages, @bookmarks = paginate(
:bookmarks,
:conditions => ['user_id = ?', current_user.id],
:order => 'position',
:per_page => 10
)
render :layout => false
end
position は1から始まりますが、配列のインデックスは 0 から始まるため、+1 しています。
@bookmark_pages ... の部分は list アクションからそのまま拝借しました。ページングが関わってきた場合の動作については、とりあえず今は考慮に入れていません。
render :layout => false で layout を使わずにレンダリングするよう指定しています。
ビューはわずか一行だけ。partial のビューを呼び出しています。
== app/views/bookmarks/sort.rhtml
<%= render :partial => 'sortable_bookmarks', :locals => {:bookmarks => @bookmarks} %>
css ファイルに以下を追加して見た目を整えます。
== public/stylesheets/base.css
/* Drag and Drop */
.draggable {
list-style: none;
cursor: -moz-grab;
background: #AAAAAA;
border: #222222 1px ridge;
padding: 7px;
margin-bottom: 7px;
}
.draggable:hover {
background: #EEEEEE;
}
#sortable_bookmarks {
width: 80%;
}
cursor: -moz-grab; は、ドラッグできる項目の上にカーソルをもっていったときに、カーソルが手の形に変わるようにするためです。
.draggable:hover ... の指定をおこない、ドラッグ可能な項目の上にカーソルをもっていったときに、背景色が変わるようにしました。
ドラッグアンドドロップは画像ではわかりにくいので、スクリーンキャストを用意してみました。
リンク先で再生してみてください。
まとめ
Acts As List を使ってモデルを並べ替え可能にし、Rails の AJAX 用ヘルパーを使ってドラッグアンドドロップの機能を実装しました。
ここまでのソースコードをアップロードしておきます。
» 10best - Google Code
svn checkout http://10best.googlecode.com/svn/trunk/ 10best
でチェックアウトするか、ブラウザでもリポジトリを閲覧することができます。




最近のコメント