« テンプレートの記述もPerlで - Template::Declareを使う | メイン | フローティングウィンドウの表示 »

ドラッグアンドドロップで並べ替え(Rails + Ajax)はてなブックマークに追加 livedoorクリップに追加 Yahoo!ブックマークに追加 del.icio.usに追加 イザ!ブックマーク ニフティクリップに追加

Ruby(とRails)を担当している石原です。

ソーシャル「OSを入れた後にインストールする10のアプリケーション」(仮) を作る過程をレポートしています。

これまでのエントリーはこちら ↓

  1. つくるぶガイドブログ: Ruby on Rails を使ってひとりでサービスを作ってみよう
  2. つくるぶガイドブログ: ひとりサービスの雛型をつくる(リキッドレイアウト、GetText、Acts as Authenticated)
  3. つくるぶガイドブログ: Rails で楽々ソーシャルブックマークの仕組みを作る
  4. つくるぶガイドブログ: 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>

下図、矢印マークをクリックすれば、順番を並べ替えられます。

sort.jpg

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 ... の指定をおこない、ドラッグ可能な項目の上にカーソルをもっていったときに、背景色が変わるようにしました。

ドラッグアンドドロップは画像ではわかりにくいので、スクリーンキャストを用意してみました。

» ドラッグアンドドロップのデモ by Jing

リンク先で再生してみてください。

まとめ

Acts As List を使ってモデルを並べ替え可能にし、Rails の AJAX 用ヘルパーを使ってドラッグアンドドロップの機能を実装しました。

ここまでのソースコードをアップロードしておきます。

» 10best - Google Code


svn checkout http://10best.googlecode.com/svn/trunk/ 10best

でチェックアウトするか、ブラウザでもリポジトリを閲覧することができます。