昨日から、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メソッドにメソッド名も渡せること。

  1. script/console
  2. rs = ActionController::Routing::Routes
  3. 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