#258 Token Fields
- Download:
- source codeProject Files in Zip (144 KB)
- mp4Full Size H.264 Video (23.9 MB)
- m4vSmaller H.264 Video (13.7 MB)
- webmFull Size VP8 Video (29.9 MB)
- ogvFull Size Theora Video (29.8 MB)
たとえば書籍販売サイトを開発しているとしましょう。まだ基本的な機能しかなく、新しい本の名前を登録できるだけです。
ある本に対して著者を登録しようと思います。このアプリケーションでは、Book
(書籍)とAuthor
(著者)が、Authorship
(執筆)という結合用モデルを介して多対多の関係にあり、それに基づいてすでにある程度コードが書かれています。Book
モデルを見てみましょう。
class Book < ActiveRecord::Base attr_accessible :name has_many :authorships has_many :authors, :through => :authorships end
この関連づけにより、書籍は複数の(has_many
)著者によって執筆されます(Authorship
)。これは標準的なhas_many :through
の関係です。
この多対多の関係を、フォームではどう扱えばいいでしょうか? 一つの方法は、チェックボックスを利用するやり方で、エピソード17[動画を見る, 読む]で紹介しました。ここでの問題は、選択対象の著者の数が多すぎて、チェックボックスを並べるのが現実的ではないという点です。そこで、たとえばテキストボックスに著者名を入力するにしたがって著者リストから自動補完され、複数の著者を登録できるようになっていれば、ずっと使いやすいのではないでしょうか? これは、Facebookでメッセージを送信するときのインターフェイスに似ていて、多対多の関係をうまく処理しています。
今回のエピソードではjQueryプラグインを使ってこの機能をRailsアプリケーションに実装する方法を紹介します。
Tokeninput(トークン入力)
これを実現する一つの選択肢は、jQuery UIのAutocomplete プラグインを利用する方法です。しかしこの場合、トークン入力を扱うためにコードをかなりカスタマイズする必要があります。他のよりよい選択肢としては、jQuery Tokeninputがあります。これはまさにここで作りたい機能を実現し、テーマをいくつかの中から選択することもできます。
このプラグインを利用するテキストフィールドは、コンマ区切りの数値id
のリストを受け取り、サーバ側で簡単に分解できます。(このidがどこから来るかは、後ほど説明します。)
プラグインは以下のファイルで構成されています。今回のアプリケーションで使用するために、jquery.tokeninput.js
ファイルをpublic/javascripts
ディレクトリにコピーし、styles
ディレクトリ内のファイルをpublic/stylesheets
にコピーします。
アプリケーションでjQueryを使用する設定をまだ行っていなかったので、jquery-rails
gemをGemfile
に追加し、bundle
コマンドを実行してインストールします。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'jquery-rails'
次のコマンドでjQueryをインストールします。
$ rails g jquery:install
最後に、TokeninputのJavaScriptとCSSのファイルを、アプリケーションのレイアウトファイルにincludeします。
<!DOCTYPE html> <html> <head> <title><%= content_for?(:title) ?yield(:title) : ↵ "Untitled" %></title> <%= stylesheet_link_tag "application", "token-input" %> <%= javascript_include_tag :defaults, "jquery.tokeninput" %> <%= csrf_meta_tag %> <%= yield(:head) %> </head> <body> <!-- Rest of file... -->
これですべての設定が終わったので、著者フィールドを追加します。まずフォームに新規のテキストフィールドを追加し、名前をauthor_tokens
とします。
<%= form_for @book do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.label :author_tokens, "Authors" %><br /> <%= f.text_field :author_tokens %> </p> <p><%= f.submit %></p> <% end %>
Book
モデルにはauthor_tokens
という属性はないので、ここでgetter、setterメソッドを追加します。
class Book < ActiveRecord::Base attr_accessible :name, :author_tokens has_many :authorships has_many :authors, :through => :authorships attr_reader :author_tokens def author_tokens=(ids) self.author_ids = ids.split(",") end end
getterメソッドには、attr_reader
を使用できます。setterの方は少し複雑で、テキストフィールドから送信されたカンマ区切りのidのリストを分解する必要があります。setterメソッドでは、受け取ったリストを別々のidに分割し、author_ids
にそのリストの値を入れて、その本の著者として設定します。最後にこの新しく作ったフィールドをattr_accessible
のリストに追加し、フォームからどちらも受け付けられるようにします。
フォームを再度読み込むと、新しいAuthors
フィールドが表示されます。idを直接このフィールドに入力することもできますが、ここではTokeninputプラグインを使いたいので、このフィールドで動作するよう設定していきます。
Tokeninputのページには、プラグインの使い方のドキュメントがあります。ここでは、テキストフィールドでtokenInput
を呼び出してURLを渡します。URLは、次のようなフォーマットのJSONを返し、ユーザが入力するにしたがって自動補完リストに項目が表示されます。
[ {"id":"856","name":"House"}, {"id":"1035","name":"Desperate Housewives"}, ... ]
リストをフィルタリングする場合は、テキストボックスのテキストがURLの検索文字列にq
という引数として渡されます。
それでは、これをアプリケーションに組み込みましょう。まずauthor_tokensにトークン入力の機能を付加するJavaScriptを記述します。このコードはapplication.js
ファイルに置きます。
$(function () { $('#book_author_tokens').tokenInput('/authors.json', { crossDomain: false }); });
このコードはid
を使ってauthor_tokensというテキストボックスを探し、それに対してtokenInput
を呼び出してプラグインを有効化してURLをわたします。するとJSONが返され、自動補完候補として表示されます。このURLは/authors.json
になります。これを処理するコードを次に記述します。いくつかのオプションを、第2引数としてtokenInput
関数に対して渡します。ここではcrossDomain:false
オプションを指定して、結果がJSONPとして送られないようにする必要があるようです。このオプションによって、応答が標準的なJSONフォーマットで送信されます。
次に、設定したURLが正しく動作するようにします。すでにAuthorsController
があるので、あとはindex
アクションがJSONリクエストに応答できるようにします。そのために、ブロック内に2つのフォーマット用のrespond_to
の呼び出しを追加します。ひとつはデフォルトのHTML、もう一つはJSONで、著者のリストをJSONフォーマットで返します。
class AuthorsController < ApplicationController def index @authors = Author.all respond_to do |format| format.html format.json { render :json => @authors } end end end
http://localhost:3000/authors.json
にアクセスすると、URLが返すJSONが表示されます。
これで著者のリストをJSONフォーマットで返すことができますが、それぞれの要素がauthor
オブジェクト内にネストされています。しかしこれはTokeninputが求める形式ではありません。引数のid
とname
が配列になっていなくてはいけません。もし処理対象のモデルがname
属性をもっていなければ、カスタマイズして追加しなければいけません。しかしここで必要なのは、リストの各要素からルートのauthorオブジェクトを削除することです。ルート項目を全体的に削除する方法はいくつかありますが、とりあえずここでは応急処置として、それぞれの著者の属性リストに配列をマッピングします。
def index @authors = Author.all respond_to do |format| format.html format.json { render :json => @authors.map(&:attributes) } end end
ここでページを再度読み込むとルート項目は削除されていて、各著者についてTokeninputが扱えるフォーマットの属性リストができています。
id
とname
以外の属性はTokeninputに無視されるので問題ありませんが、本番のアプリケーションでは送信データを最小限にするために削除することを検討した方がいいでしょう。
新規書籍の登録フォームにアクセスして試してみましょう。著者フィールドでタイピングを始めるとすべての著者のリストが返されるのがわかります。
ここは、すべての著者ではなく、名前が検索語に一致する著者だけが返されるように変更します。AuthorsController
で、検索文字列の引数q
でコントローラに渡されたテキストボックスのテキストによって、全著者のリストをフィルタリングします。
def index @authors = Author.where("name like ?", "%#{params[:q]}%") respond_to do |format| format.html format.json { render :json => @authors.map(&:attributes) } end end
Author.all
をAuthor.where
に置き換え、渡された検索語にLIKEで部分一致する著者名を検索します。検索語が%記号で挟まれているので、名前フィールドの一部分でも一致したものがヒットします。もう一度著者を検索すると、今度は検索にヒットした名前だけが返されます。
これで自動補完フィールドが正しくフィルタリングしているので、試しに本を追加してみましょう。著者が二人いる書籍を作成すると正しく保存され、その後リダイレクトされたその書籍のページをみると著者が二人表示されているのがわかります。
書籍情報を編集する
しかしここで書籍情報を編集するときに問題があります。フォームに書籍の著者が表示されません。表示の前にフォームに著者名を渡さなくてはいけません。
TokeninputプラグインにはprePopulate
というオプションがあり、JSONデータを渡すとそれに基づいてリストデータを生成します。application.js
ファイル内でtokenInput
を呼び出すときにこのオプションを追加できますが、関連のデータをどう渡せばいいのでしょうか? ひとつの方法は、テキストフィールドにHTML 5データ属性を追加してそこからデータを読むやりかたです。今回はこの方法を試してみましょう。
<%= form_for @book do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.label :author_tokens, "Authors" %><br /> <%= f.text_field :author_tokens, "data-pre" => ↵ @book.authors.map(&:attributes).to_json %> </p> <p><%= f.submit %></p> <% end %>
属性をdata-pre
と呼ぶことにします。その値は、自動補完リスト用にJSONを生成するのと同じ方法で、書籍の著者の属性に設定されます。
このデータをJavaScriptファイルから読めるので、著者リストを表示できるようになります。
$(function () { $('#book_author_tokens').tokenInput('/authors.json', { crossDomain: false, prePopulate: $('#book_author_tokens').data('pre') }); });
編集ページを読み込むと、著者リストにデータが表示されるのがわかります。
著者情報を更新してみましょう。例えば、一人削除して別の一人を追加すると、どちらも正しく更新されます。
テーマ
テーマは現在Tokeninputプラグインに標準で含まれるデフォルトのものを使っています。これを変更する場合は2ヶ所を修正する必要があります。一つ目はレイアウトファイルで、Tokeninput CSSファイルをデフォルトのtoken-input
から希望のものに変更します。Facebookというテーマが利用できるので、デモの目的で使用してみます。
<%= stylesheet_link_tag "application", "token-input-facebook" %>
次にTokeninputフィールドを生成するJavaScriptファイルを編集して、テーマオプションを変更します。
/public/javascripts/application.js
$(function () { $('#book_author_tokens').tokenInput('/authors.json', { crossDomain: false, prePopulate: $('#book_author_tokens').data('pre'), theme: 'facebook' }); });
ここでは好きなテーマを設定し、アプリケーションに合うようにカスタマイズ可能です。編集ページを再読み込みすると、新しいテーマが適用されているのがわかります。
今回のエピソードはこれで終わりです。TokeninputはRailsのフォームで多対多の関係を処理する優れた方法で、その素晴らしさをここで示すことができたと願っています。