昨日から、auto_completeとacts_as_taggable_on_steroidを一緒に使うことを試みている。
よくある、サンプルのアプリケーション、blogのArticle(model)をTaggableにする。で、そのタグのフィールドをauto_completeにして、view上で、過去に入力されたタグから選択できるようにしたい。結局、auto_complete pluginを、多少、修正したけど、ちゃんと動いた。わーい。以下に、必要な変更/修正をまとめた。
GET Methodを使う
昨日は、Rails2.0.2で導入された、InvalidAuthenticityTokenの例外を発生させないようにするために、Ajaxがタグのリストをリクエストする時に、ちゃんと、セキュリティのトークンをセットするように、auto_complete pluginを修正した。これでも良いのだけど、もう一つの方法は、リクエストをPOSTじゃなくてGETで送るという手がある。RESTfulのアプリケーションという意味では、タグのリストを取るだけではModelの状態を変化させないから、GETの方がベター。
<%= text_field_with_auto_complete:article, :tag_list, {}, {:tokens => ',', <span style="color:#0033FF;"> :method => :get</span>} %>
routeを定義する
methodにgetを指定したけど、何故かうまく、希望のactionにroutingされない。で、googleしたら、routes.rbをいじくる方法が書かれていた。それで、自分のroutes.rbを以下のようにしてみた。
map.resources :articles, :collection => {:auto_complete_for_tag_article_tag_list => :get} do |article| article.resources :comments end
最初の行で、articlesコントローラーに、新しいアクションauto_complete_for_tag_article_tag_listへのルートを追加している。このルートはGETメソッドにのみ適用される。routes.rbはrailsコンソールからテストできる。知らなかったのは、recognize_pathメソッドにメソッド名も渡せること。
- script/console
- rs = ActionController::Routing::Routes
- rs.recognize_path "/some/path", :method => :get
とか。
taggingsテーブルをjoinする
リクエストが無事にコントローラーに届いたら、Tagテーブルからタグのリストを取り出すのだけど、この時に、taggingsテーブルとのjoinが必要になる。auto_complete pluginオリジナルのauto_complete_for(object, method, options = {})メソッドでも、ぐちゃぐちゃとoptionsを渡せば使えそうだったけど、面倒くさいのでauto_complete_for_tag(object, options = {}) というメソッドを作った。
ついでに、auto_complete_macros_helperにも、使いやすいように(自分にとって)、text_field_with_auto_complete_for_tagを追加した。
コードをまとめると、
auto_complete_macros_helper.rb
# Wrapper for text_field with added AJAX autocompletion functionality. # # In your controller, you'll need to define an action called # auto_complete_for to respond the AJAX calls, # def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}) text_field_with_auto_complete_common(object, method, tag_options, completion_options) + auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options)) end def text_field_with_auto_complete_for_tag(object, method = 'tag_list', tag_options = {}, completion_options = {}) text_field_with_auto_complete_common(object, method, tag_options, completion_options) + auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_tag_#{object}_#{method}" } }.update(completion_options)) end private def text_field_with_auto_complete_common(object, method, tag_options = {}, completion_options = {}) (completion_options[:skip_style] ? "" : auto_complete_stylesheet) + text_field(object, method, tag_options) + content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") end
auto_complete.rb
def auto_complete_for_tag(object, options = {}) define_method("auto_complete_for_tag_#{object}_tag_list") do find_options = { :conditions => [ "LOWER(name) LIKE ? and taggable_type = ?", '%' + params[object]['tag_list'].downcase + '%', object.to_s], :joins => "inner join taggings as tg on `tags`.id = tg.tag_id", :order => "name ASC", :limit => 10 }.merge!(options) @items = Tag.find(:all, find_options) render :inline => "<%= auto_complete_result @items, 'name' %>" end end
_form.html.erbパーシャル
<p> <label for="article_tag_list">Tag</label><br/> <%= text_field_with_auto_complete_for_tag :article, :tag_list, {}, {:tokens => ',', :method => :get} %> </p>
article_controller.rb
class ArticlesController < ApplicationController auto_complete_for_tag :article
article.rb
class Article < ActiveRecord::Base acts_as_taggable has_many :comments end