#102 Auto-Complete Association (revised)
- Download:
- source codeProject Files in Zip (95.6 KB)
- mp4Full Size H.264 Video (16.9 MB)
- m4vSmaller H.264 Video (9.6 MB)
- webmFull Size VP8 Video (11.9 MB)
- ogvFull Size Theora Video (21.5 MB)
例えばたくさんの商品を管理する簡単なアプリケーションがあって、それぞれの商品がある分類に属しているとします。このアプリケーションに商品を追加するためのフォームが下の画面で、新しい商品の分類を選択するためのドロップダウンリストがあります。
このアプローチは、分類の数が少ないときは機能しますが、選択肢が増えるとドロップダウンリストはとたんに煩わしくなります。今回のエピソードでは、このドロップダウンの代わりに、テキストフィールドを用いてユーザが分類名を入力し始めると自動補完する機能を実装します。
フォームを表示するテンプレートを以下に示します。collection_select
を使用して、関連の分類のドロップダウンを生成しています。
<%= form_for(@product) do |f| %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :price %><br /> <%= f.text_field :price %> </div> <div class="field"> <%= f.label :category_id %><br /> <%= f.collection_select :category_id, Category.order(:name), :id, :name, include_blank: true %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
collection_select
をテキストフィールドに置き換えて名前をcategory_name
とし、ユーザはそこに分類をタイプ入力することにします。
<div class="field"> <%= f.label :category_name %><br /> <%= f.text_field :category_name %> </div>
フォームをリロードすると、Product
モデルにはcategory_name
という属性はないのでエラーメッセージが表示されます。これを解決するために仮の属性(virtual attribute)を作成します。
class Product < ActiveRecord::Base belongs_to :category def category_name category.try(:name) end def category_name=(name) self.category = Category.find_by_name(name) if name.present? end end
category_name
用のgetterとsetterメソッドを作成しました。getterは関連するCategory
の名前を返します(ただし関連する分類がなかった場合にはnil
を返すようにtry
を使っていることに注意してください)。setterは、name
が存在する場合は一致する名前のCategory
にその商品のcategory
を設定します。
フォームをリロードすると、category name(分類名)フィールドがあり、存在するカテゴリとともに商品を追加すると、カテゴリの関連の組み合わせとともに商品がデータベースに追加されます。
しかし、誰かが新規のカテゴリで商品を作成したらどうなるでしょうか。このような場合には商品が作成されると同時に、新規のカテゴリも作成されれば便利でしょう。Railsではこれを簡単に実現できます。必要な作業は、setterのfind_by_name
をfind_or_create_by_name
に置き換えるだけです。
def category_name=(name) self.category = Category.find_or_create_by_name(name) if name.present? end
これで、新規のカテゴリの商品を追加したときにはカテゴリも同時に作成されるようになりました。
自動補完機能を追加する
本来の目的は自動補完機能を付加することでした。それによって、ユーザがカテゴリのフィールドに文字を入力すると、それに一致するカテゴリが表示されるようにします。Rails 3.1アプリケーションでこれを実現する一番簡単な方法は、Autocomplete widgetに含まれるjQuery UIを用いる方法で、今回はこのアプローチをとります。
JQuery UIはRails 3.1アプリケーションではすでに利用可能で、application.js
マニフェストファイルでrequireするだけです。
// This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // //= require jquery //= require jquery-ui //= require jquery_ujs //= require_tree .
products.js.coffeeファイルに自動補完機能を追加します。
jQuery -> $('#product_category_name').autocomplete source: ['foo', 'food', 'four']
このコードではまずDOMがロードされたかどうかをチェックし、ロードされたら分類名のフィールドをid
で検索しautocomplete
を呼び出します。この関数はsource
オプションをとり、これにより自動補完する候補の元を指定します。URLを渡した場合はAJAXリクエストを発行して戻された値を候補として使用します。あるいは、候補の配列を渡します。簡単に機能するようにしたいので、ここではとりあえず配列を使用しました。
商品の新規作成ページにアクセスして、配列中のいくつかの項目に一致する名前を入力し始めると、自動補完された候補リストが表示されます。
自動補完リストはうまく機能しましたが、見た目はよくありません。productsのSCSSファイルに次のコードを追加して、これを改良します。
ul.ui-autocomplete { position: absolute; list-style: none; margin: 0; padding: 0; border: solid 1px #999; cursor: default; li { background-color: #FFF; border-top: solid 1px #DDD; margin: 0; padding: 0; a { color: #000; display: block; padding: 3px; } a.ui-state-hover, a.ui-state-active { background-color: #FFFCB2; } } }
ページをリロードすると、リストはずっときれいになりました。
自動補完の値をデータベースから取得する
リストがきれいに表示されるようになったので、その中身を配列からの値ではなく、データベースから一致するカテゴリを取得することに集中したいと思います。これを実現する方法は2つあり、ここではその両方を紹介します。
1つ目は、すべてをクライアント側に持っておく方法です。これは選択肢の数がそれほど多くない場合に適した方法で、今回もそのケースに該当します。カテゴリ名のテキストフィールドのデータ属性にすべての選択肢を埋め込みます。
<div class="field"> <%= f.label :category_name %><br /> <%= f.text_field :category_name, data: {autocomplete_source: Category.order(:name).map(&:name)} %> </div>
このために、テキストフィールドに data
オプションを設定して(このdata
ハッシュはRails 3.1で新たに導入された機能で、データ属性を設定する便利な方法です)、カテゴリ名の配列を渡します。ページをリロードしてソースを見ると、これの結果がわかります。
<div class="field"> <label for="product_category_name">Category name</label><br /> <input data-autocomplete-source="["Beverages","Board Games","Books","Breads","Canned Foods","Clothes","Computers","Dry Foods","Frozen Foods","Furniture","Headphones","Magazines","Music","Other Electronics","Pastas","Portable Devices","Produce","Snacks","Software","Televisions","Toys","Video Games","Video Players","Videos"]" id="product_category_name" name="product[category_name]" size="30" type="text" /> </div>
テキストフィールドにdata-autocomplete-source
属性が追加され、そこにはカテゴリ名が含まれています。これらの値はJSON文字列に変換され、HTMLエスケープされています。ここでautocomplete
のダミーデータをこの属性からのデータに置き換えます。
jQuery -> $('#product_category_name').autocomplete source: $('#product_category_name').data('autocomplete-source')
ページをリロードしてカテゴリ名フィールドに文字を入力すると、自動補完リストに一致するカテゴリが表示されます。
これだけでクライアント側で自動補完機能を実現できました。カテゴリの数が限られる今回のような場合にはこのアプローチで十分です。しかし違う状況の場合もあるでしょう。例えば、選択肢が数百数千ありすべての候補をクライアント側に持つことが現実的ではないような場合です。このような場合には、HTMLドキュメントに値を埋め込む代わりに、AJAXリクエストを使ってサーバから自動補完候補を取得する方法の方が適しているでしょう。
これをおこなうためには、autocomplete
関数に配列ではなくURLを渡します。data-autocomplete-source
属性の中のデータをURLに置き換えます。AJAXリクエストはカテゴリのリストを返さなくてはいけないので、categories_path
のURLを使用します。
<div class="field"> <%= f.label :category_name %><br /> <%= f.text_field :category_name, data: {autocomplete_source: categories_path} %> </div>
CategoriesControllerはまだないので、ここで作成します。
$ rails g controller categories
In order for the categories_path
method to be defined we’ll modify the routes file and add a categories
resource.
Store::Application.routes.draw do root to: 'products#index' resources :products resources :categories end
新しいCategoriesController
の中にindex
アクションを書きます。これがテキストフィールドに入力された文字列に一致するカテゴリを取得し、JSONデータとして返します。
class CategoriesController < ApplicationController def index @categories = Category.order(:name).where("name like ?", "#{params[:term]}") render json: @categories.map(&:name) end end
autocomplete widgetからtext
フィールドのテキスとがtermパラメータとして渡されるので、そのパラメータを用いてカテゴリにフィルタをかけます。そしてこのフィルタがかけられたリストを、名前の配列として返します。
ページをリロードしてカテゴリ名を入力し始めると、一致するカテゴリがサーバから取得されます。
テキストフィールド内のテキストが変わるごとにAJAXリクエストが発行されるので自動補完リストは少し遅くなりますが、ページがロードされるたびにクライアントにカテゴリの全リストを送る必要はなくなりました。
関連データで自動補完を使用する今回のエピソードは以上です。動作のしくみと使用できるオプションについてさらに情報が必要な場合は、ドキュメントを参照することをお勧めします。例えばminLengthオプションを渡して何文字入力されたらAJAXリクエストを発行するかを指定するなどが可能です。
多対多の関係において自動補完が必要な場合は、まさにそれを扱ったエピソード258を参照してください。