検索可能なプルダウンリストを作る。

selectタグはマウスだけで簡単に選択できて便利なんだけど、選択項目の件数が増えた時にちょっと困ったことになる。例えば、全銀データで利用する銀行コードは1500件、支店コードは35000件の中から選択する状況の場合、selectタグだと長〜〜〜い選択リストを作ることになる。やってやれないことはないが、その中から目的の一つまでスクロールして見つけるのは大変だ...。
それでは、実際に本家の銀行が、webページ上の振込サービスでどんな方法で選択するようになっているか見てみると...

  • テキストフィールドに検索したい銀行名を入力する。
  • 検索ボタンを押すと、検索ページに移動して、絞り込まれた銀行リストから選択する。
  • 銀行が確定したら、今度はテキストフィールドに検索したい支店名を入力する。
  • 同じように絞り込まれた支店リストから選択する。

んー...手順に従った確実な方法と言えるけど、あまりスマートな方法ではない。(銀行支店を選択するだけで、4回もページ遷移するなんて...。)この方法で振込先を10件、新規登録するのはかったるい。
それでは、どうするべきか...。こんな時のために、Railsはauto_complete_fieldやtext_field_with_auto_completeというヘルパメソッドを用意してくれている!さっそく使ってみる。

シンプルな基本型

以下のような振込先情報を保存するテーブルがあったとして、今まで銀行コードを入力するtext_fieldが存在した場合...

  create_table "payee_accounts" do |t|
    t.column "bank_id",         :integer # 銀行コード
    t.column "branch_id",       :integer # 支店コード
    t.column "account_type_id", :integer # 口座種
    t.column "account_num",     :integer # 口座番号
    t.column "account_kana",    :string  # 口座名義
  end

railsのドキュメント通りにやってみると、以下のように追記すると、これまでの銀行コードの入力履歴が、入力した文字に連動して絞り込まれ、選択リストとして表示される。

<%# ビュー %>
<%= text_field_with_auto_complete :payee_account, :bank_id %>
# コントローラー
auto_complete_for :payee_account, :bank_id

こんな感じ...

素晴らしいシンプルさ!でも、実現したいプルダウンリストの姿には程遠い...。ここで実現したいことはcollection_selectのような動き。銀行名のリストが表示されて、選択すると対応するbanksテーブルのidがパラメーターとして渡されるようにしたいのだ。

text_field_with_auto_completeとauto_complete_forで何が起こっているのか?

シンプルな2行のコードの内容がわかれば、目指すプルダウンリストが作れるのではと考え、調べてみた。なかなかのコード量だ...。

  • ビューに定義されたtext_field_with_auto_complete :payee_account, :bank_idは、文字が入力されると以下のようなhtmlを出力してくれる。
<!-- 選択リストを表示するdivブロックのスタイルシートを指定している。 -->
<style>     div.auto_complete {
              width: 350px;
              background: #fff;
              }
            div.auto_complete ul {
              border:1px solid #888;
              margin:0;
              padding:0;
              width:100%;
              list-style-type:none;
              }
            div.auto_complete ul li {
              margin:0;
              padding:3px;
              }
            div.auto_complete ul li.selected { 
              background-color: #ffb; 
              }
            div.auto_complete ul strong.highlight { 
              color: #800; 
              margin:0;
              padding:0;
              }
</style>

<!-- 通常のtext_fieldが出力するinputタグ、autocomplete="off"でブラウザ側のオートコンプリート機能を無効にしている。 -->
<input type="text" size="30" name="payee_account[bank_id]" id="payee_account_bank_id" autocomplete="off"/>

<!-- 選択リストを表示するdivブロック、ここの内容が選択リストとして表示される。 -->
<div id="payee_account_bank_id_auto_complete" class="auto_complete" 
     style="position: absolute; left: 97px; top: 363px; width: 256px; opacity: 0.999999; display: none;">
  <ul>
    <li class="selected">16</li>
    <li>129</li>
    <li>138</li>
    <li>291</li>
    <li>9491</li>
  </ul>
</div>

<!-- オートコンプリート機能を呼び出すJavaScript -->
<script type="text/javascript">
//<![CDATA[
var payee_account_bank_id_auto_completer = 
  new Ajax.Autocompleter('payee_account_bank_id', // キー入力を監視するtext_fieldのid属性
  'payee_account_bank_id_auto_complete', // 選択リストを描画するdivタグのid属性
  '/slips/auto_complete_for_payee_account_bank_id', {}) // キー入力が発生した時に実行するアクション
//]]>
</script>
  • コントローラーに定義されたauto_complete_for :payee_account, :bank_idは、コントローラーに以下のメソッドを追加してくれる。
def auto_complete_for_payee_account_bank_id
  find_options = { 
    :conditions => [ "LOWER(bank_id) LIKE ?", '%' + params[:payee_account][:bank_id].downcase + '%' ], 
    :order => "bank_id ASC",
    :limit => 10 }

  @items = PayeeAccount.find(:all, find_options)

  render :inline => "<%= auto_complete_result @items, 'bank_id' %>"
end
  • さらに上記render :inlineが指定する<%= auto_complete_result @items, 'bank_id' %>は、余分なことを省略して書けば、以下のrhtmlテンプレートと同等の結果を出力してくれる。
<ul>
  <% @items.each do |item| %>
    <li><%= item.bank_id %></li>
  <% end %>
</ul>

展開されたコードを元にアレンジ

以下のような銀行テーブルがあったとして...

id name kana
1 みずほ ミズホ
5 三菱東京UFJ ミツビシトウキョウUFJ
9 三井住友 ミツイスミトモ
... ... ...
<%# ビュー %>
<%= text_field_with_auto_complete :bank, :kana %>
# コントローラー
def auto_complete_for_bank_kana
  find_options = { 
    :conditions => [ "LOWER(kana) LIKE ?", '%' + params[:bank][:kana].downcase + '%' ], 
    :order => "kana ASC",
    :limit => 10 }

  @items = Bank.find(:all, find_options)

  render :inline => "<%= auto_complete_result @items, 'kana' %>"
end

これで、このように表示される。

  • 銀行テーブルのkana列をリストとして表示している。(振込先情報の入力履歴からのリストではない。)
  • しかし、選択して渡される値は、idでなく、kana列の情報になっている。

id情報を渡したい

  • id列の情報を渡したいので、hidden_fieldを1行追加してしまった。(一つのinputフォームに表示用の値と、パラメーターとして送信する値を、区別して保持することが出来ないと思ったので。)追加したhidden_fieldは、パラメーター送信用の値idを保持する目的だ。
<%# ビュー %>
<%= text_field_with_auto_complete :bank, :kana %>
<%= hidden_field :payee_account, :bank_id

リストを選択したら、2箇所のフォームを更新する

すると、新たな問題が...。オートコンプリートでリストを選択した時に、2箇所のフォームを更新することが必要になってしまった...。悩んだ結果、text_field_with_auto_completeでは、詳細なオプションが指定できないので、auto_complete_fieldを使ってみることにした。以下のように書き直してみた。
詳細なオプション指定、出来ました。オプションを指定する位置が違っていて出来ないと思い込んでいたのでした...。

<%# ビュー %>
<%= text_field_with_auto_complete :bank, :kana %>

上記コードは、以下と同等だ。

<%# ビュー %>
<%= auto_complete_stylesheet %> 
<%= text_field :bank, :kana %>
<div id="bank_kana_auto_complete" class="auto_complete"></div>
<%= auto_complete_field :bank_kana, 
                        :url=>{:action=>'auto_complete_for_bank_kana'} %>

そして、:after_update_elementオプションを指定して、以下のようにしてみた。

  • 選択リストを描画する時のliタグのid属性に、bank.idの値を設定している。
  • :after_update_elementの中でそのid属性値を取り出して、hidden_fieldの値として代入している。
<%# ビュー %>
<%= auto_complete_stylesheet %> 
<%= text_field :bank, :kana %>
<div id="bank_kana_auto_complete" class="auto_complete"></div>
<%= auto_complete_field :bank_kana, 
                        :url=>{:action=>'auto_complete_for_bank_kana'}, 
                        :after_update_element=>"function(element, selectedItem){$('payee_account_bank_id').value = selectedItem.id;}" %>
<%= hidden_field :payee_account, :bank_id %>

text_field_with_auto_completeに:after_update_elementオプションを指定できると分かったので、以下のように変更してみた。簡潔になった!

  • text_field_with_auto_completeのオプションは、tag_optionsとcompletion_optionsに分かれていることに注意。
    • tag_optionsは、text_fieldに対するオプション設定になる。
    • completion_optionsは、auto_complete_fieldに対するオプション設定になる。
<%# ビュー %>
<%= text_field_with_auto_complete :bank, :kana, 
      {}, :after_update_element=>"function(element, selectedItem){$('payee_account_bank_id').value = selectedItem.id;}" %>
<%= hidden_field :payee_account, :bank_id %>
# コントローラー
def auto_complete_for_bank_kana
  find_options = { 
    :conditions => [ "LOWER(kana) LIKE ?", '%' + params[:bank][:kana].downcase + '%' ], 
    :order => "kana ASC",
    :limit => 10 }

  @items = Bank.find(:all, find_options)

  render :partial => 'auto_complete_for_bank_kana'
end
  • ここで、liタグのid属性にbank.idを設定している。
<%# ビュー_auto_complete_for_bank_kana.rhtml %>
<ul>
  <% @items.each do |item| %>
    <li id=<%= item.id %>><%= item.kana %></li>
  <% end %>
</ul>
      • 以下のように、字下げと改行を入れて書いてしまうと、銀行名の前後に不要なスペースが入ってしまい、悩むことになる。どうやら、普段は描画されないliタグ中の<%= item.kana %>の直前のスペース6文字分と直後のスペース4文字分も、オートコンプリートの選択リストを描画する時には、しっかり表示されてしまうようだ。上記のように無駄な字下げや改行を入れないで1行で書いておいた。
<%# 以下、無駄なスペースが入ってしまう例 %>
<ul>
  <% @items.each do |item| %>
    <li id=<%= item.id %>>
      <%= item.kana %>
    </li>
  <% end %>
</ul>

これでtext_field_with_collection_auto_complete的な利用が出来るようになった!