homeASCIIcasts

258: トークンフィールド 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Naomi Fujimoto

たとえば書籍販売サイトを開発しているとしましょう。まだ基本的な機能しかなく、新しい本の名前を登録できるだけです。

簡単な書籍販売サイト

ある本に対して著者を登録しようと思います。このアプリケーションでは、Book(書籍)とAuthor(著者)が、Authorship(執筆)という結合用モデルを介して多対多の関係にあり、それに基づいてすでにある程度コードが書かれています。Bookモデルを見てみましょう。

/app/models/book.rb

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があります。これはまさにここで作りたい機能を実現し、テーマをいくつかの中から選択することもできます。

jQuery Tokeninputサイトのデモ画面

このプラグインを利用するテキストフィールドは、コンマ区切りの数値idのリストを受け取り、サーバ側で簡単に分解できます。(このidがどこから来るかは、後ほど説明します。)

プラグインは以下のファイルで構成されています。今回のアプリケーションで使用するために、jquery.tokeninput.jsファイルをpublic/javascriptsディレクトリにコピーし、stylesディレクトリ内のファイルをpublic/stylesheetsにコピーします。

Tokeninputプラグインのディレクトリ構造

アプリケーションでjQueryを使用する設定をまだ行っていなかったので、jquery-rails gemをGemfileに追加し、bundleコマンドを実行してインストールします。

/Gemfile

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します。

/app/views/layouts/application.html.erb

<!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とします。

/app/views/books/_form.html.erb

<%= 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メソッドを追加します。

/app/models/book.rb

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プラグインを使いたいので、このフィールドで動作するよう設定していきます。

authorフィールドに直接idを追加する

Tokeninputのページには、プラグインの使い方のドキュメントがあります。ここでは、テキストフィールドでtokenInputを呼び出してURLを渡します。URLは、次のようなフォーマットのJSONを返し、ユーザが入力するにしたがって自動補完リストに項目が表示されます。

[
    {"id":"856","name":"House"},
    {"id":"1035","name":"Desperate Housewives"},
    ...
]

リストをフィルタリングする場合は、テキストボックスのテキストがURLの検索文字列にqという引数として渡されます。

それでは、これをアプリケーションに組み込みましょう。まずauthor_tokensにトークン入力の機能を付加するJavaScriptを記述します。このコードはapplication.jsファイルに置きます。

/public/javascripts/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フォーマットで返します。

/app/controllers/authors_controller.rb

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が表示されます。

authors.jsonから返されたJSON

これで著者のリストをJSONフォーマットで返すことができますが、それぞれの要素がauthorオブジェクト内にネストされています。しかしこれはTokeninputが求める形式ではありません。引数のidnameが配列になっていなくてはいけません。もし処理対象のモデルがname属性をもっていなければ、カスタマイズして追加しなければいけません。しかしここで必要なのは、リストの各要素からルートのauthorオブジェクトを削除することです。ルート項目を全体的に削除する方法はいくつかありますが、とりあえずここでは応急処置として、それぞれの著者の属性リストに配列をマッピングします。

/app/controllers/authors_controller.rb

def index
  @authors = Author.all
  respond_to do |format|
    format.html
    format.json { render :json => @authors.map(&:attributes) }
  end
end

ここでページを再度読み込むとルート項目は削除されていて、各著者についてTokeninputが扱えるフォーマットの属性リストができています。

Tokeninputが分解できる形式の修正版JSON

idname以外の属性はTokeninputに無視されるので問題ありませんが、本番のアプリケーションでは送信データを最小限にするために削除することを検討した方がいいでしょう。

新規書籍の登録フォームにアクセスして試してみましょう。著者フィールドでタイピングを始めるとすべての著者のリストが返されるのがわかります。

自動補完リストがすべての著者を表示する

ここは、すべての著者ではなく、名前が検索語に一致する著者だけが返されるように変更します。AuthorsControllerで、検索文字列の引数qでコントローラに渡されたテキストボックスのテキストによって、全著者のリストをフィルタリングします。

/app/controllers/authors_controller.rb

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.allAuthor.whereに置き換え、渡された検索語にLIKEで部分一致する著者名を検索します。検索語が%記号で挟まれているので、名前フィールドの一部分でも一致したものがヒットします。もう一度著者を検索すると、今度は検索にヒットした名前だけが返されます。

リストはテキストボックスの文字列でフィルタリングされる

これで自動補完フィールドが正しくフィルタリングしているので、試しに本を追加してみましょう。著者が二人いる書籍を作成すると正しく保存され、その後リダイレクトされたその書籍のページをみると著者が二人表示されているのがわかります。

保存後、書籍に著者が正しく追加されている

書籍情報を編集する

しかしここで書籍情報を編集するときに問題があります。フォームに書籍の著者が表示されません。表示の前にフォームに著者名を渡さなくてはいけません。

書籍情報の編集時に著者が表示されない

TokeninputプラグインにはprePopulateというオプションがあり、JSONデータを渡すとそれに基づいてリストデータを生成します。application.jsファイル内でtokenInputを呼び出すときにこのオプションを追加できますが、関連のデータをどう渡せばいいのでしょうか? ひとつの方法は、テキストフィールドにHTML 5データ属性を追加してそこからデータを読むやりかたです。今回はこの方法を試してみましょう。

/app/views/books/_form.html.erb

<%= 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ファイルから読めるので、著者リストを表示できるようになります。

/public/javascripts/application.js

$(function () {
  $('#book_author_tokens').tokenInput('/authors.json', { 
    crossDomain: false,
    prePopulate: $('#book_author_tokens').data('pre')
  });
});

編集ページを読み込むと、著者リストにデータが表示されるのがわかります。

著者が表示されるようになった

著者情報を更新してみましょう。例えば、一人削除して別の一人を追加すると、どちらも正しく更新されます。

著者情報が正しく更新される

テーマ

テーマは現在Tokeninputプラグインに標準で含まれるデフォルトのものを使っています。これを変更する場合は2ヶ所を修正する必要があります。一つ目はレイアウトファイルで、Tokeninput CSSファイルをデフォルトのtoken-inputから希望のものに変更します。Facebookというテーマが利用できるので、デモの目的で使用してみます。

/app/views/layouts/application.html.erb

<%= 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'
  });
});

ここでは好きなテーマを設定し、アプリケーションに合うようにカスタマイズ可能です。編集ページを再読み込みすると、新しいテーマが適用されているのがわかります。

Facebookテーマを用いた自動補完テキストフィールド

今回のエピソードはこれで終わりです。TokeninputはRailsのフォームで多対多の関係を処理する優れた方法で、その素晴らしさをここで示すことができたと願っています。