Railsでtypeahead.jsを使ったオートコンプリート機能(入力補完機能)の実装手順

公開日:2015/03/18 更新日:2015/03/18
Railsでtypeahead.jsを使ったオートコンプリート機能(入力補完機能)の実装手順のサムネイル

はじめに

Railsアプリ内の入力フォームでユーザの入力に合わせた候補を表示するオートコンプリート機能をTwitter社が提供しているtypeahead.jsを使って実装したのでその手順をメモしておきます。

やりたいこと

以下のgifのように、ユーザがフォームで入力を行った時に、その入力内容に合わせた候補を自動で表示するというオートコンプリート機能を実装します。以下のようにNameのテキストフィールドではNameの候補を表示し、EmailのテキストフィールドではEmailの候補を表示できるようにします。なお、以下の例では新規ユーザ作成フォームでの名前とE-mailの入力に対してオートコンプリート機能を実装した例になりますが、これ自体は実用上はなんの意味もないです。

typeaheadimage.gif このような機能をお手軽に実装するのにtypeahead.jsというライブラリをTwitter社が提供しているので、これを使います。

typeahead.jsの動作概要

以下の図がtypeahead.jsの簡単な動作イメージになります。

typeahead-image2.001.png 上記のような動作を実現するために、以下の作業が必要になります。

  • Viewでユーザの入力を検知してtypeaheadを動作させるjavascriptの追加
  • ViewからControllerにパラメータを送るためにroutes.rbにルートを追加(routes.rbの修正)
  • パラメータを元に候補ワードを検索し、Viewに返すメソッドをControllerに追加(Controllerの修正)
  • ViewのフォームにHTMLのclass属性とid属性を付与(Viewの修正)
以降では上記の4つについてメモしていきます。なお、以降では、ユーザが入力した内容を入力ワード(上図中のuser)、そして入力ワードに対してtypeaheadによって表示する候補(上図中のadminuser, staffuser, ...)を候補ワードと呼ぶことにします。紛らわしいかもしれないのでここに明言しておきます。

環境

  • Rails 4.2.0
  • Ruby 2.2.0
  • 今回の例として使用しているRailsアプリはrails generate scaffold User name:string email:stringで作成したアプリです。Userモデル(nameカラム、emailカラムを持つ)とUserコントローラのみを持つアプリです。

typeaheadを使用するための準備

typeahead.jsをダウンロード

まず準備として、以下のサイトからtypeahead.jsをダウンロードします。以下URLにアクセスし、右側にある「Download ZIP」というボタンをクリックしてダウンロードします。

twitter/typeahead.js | GitHub

ダウンロードしたZIPファイルを解凍すると、解凍したフォルダ内にdistという名前のフォルダがあります。この中のtypeahead.bundle.min.jsというファイルを使用します。他のファイルとの違いは、上記URLサイトにも書いてあるように、typeahead.jsbloodhound.jsがオートコンプリート実装に必要なjavascript本体ファイルで、それ以外のminが付いているものは圧縮版、bundleが付いているものはtypeahead.jsbloodhound.jsの両方を含むファイルです。したがって、typeahead.bundle.min.jsもしくはtypeahead.bundle.jsのいずれかがあればオートコンプリート機能を実装できます。

今回はtypeahead.bundle.min.jsを使うので、このファイルを各自のRailsディレクトリのapp/assets/javascriptsにコピーすればOKです。

typeahead用のCSSファイルをダウンロード

typeaheadによるオートコンプリートで表示される候補ワードの見栄えを良くするために、以下のURLからtypeahead用のCSSファイルを取得し、app/assets/stylesheetsに配置しておきます。

LESS / CSS code for using typeahead.js with Bootstrap 3 | GitHub
以上でtypeaheadを使用する準備は完了です。

typeaheadを動作させるjavascriptファイル作成

Viewでのユーザの入力を検出して、入力ワードをControllerにajax通信で送信し、Controllerから候補ワードを受け取って表示するjavascriptファイルを作成しておきます。このjavascriptファイルの中でtypeaheadを使用します。ここでは作成するファイルをautocomp.jsという名前にし、以下の内容を記述します。

app/assets/javascripts/autocomp.js
var baseurl = "/typeahead?term=%QUERY"

var qst1 = { item: 'name' } //'name'はカラム名

var url1 = baseurl + "&" + jQuery.param(qst1)
// jQuery.param(params)はオブジェクトqst1をシリアライズ
// url1 =  "/typeahead?term=%QUERY&item=name"

var usernames = new Bloodhound({

  datumTokenizer: function(d) {return Bloodhound.tokenizers.whitespace([d.name]); }, //d.nameのnameはカラム名
  queryTokenizer: Bloodhound.tokenizers.whitespace,

  remote: {
            url: url1, //上記で定義したurl1を指定
            wildcard: ‘%QUERY’ // こちらが無いと動作しないとご指摘頂きました。ありがとうございます。
        }
});

// 上記で定義したusernamesの初期化
usernames.initialize();

  jQuery( document ).ready(function( $ ) {
      $('#user_name.typeahead').typeahead({ // #user_nameは後ほどViewファイルのフォーム部分に付与するid属性名
      hint: true,
      highlight: true,
      minLength: 1
    },
    {
      name: 'name',       // 'name'はカラム名
      displayKey: 'name', // 'name'はカラム名
      source: usernames.ttAdapter()
  }).on("typeahead:selected typeahead:autocomplete", function(e, datum) {
    return $('#user_name.typeahead').val(datum.name); 
    // #user_nameは後ほどViewファイルのフォーム部分に付与するid属性名
    // datum.nameのnameはカラム名
  });
});

上記の簡単な説明を以下の載せます。

  • baseurl = "/typeahead?term=%QUERY"は、ユーザの入力ワードの送信先となるURLになります。後ほど/typeaheadというURLとユーザの入力ワードを元に候補ワードを返す処理を行うControllerのメソッドを結びつけるルートをroutes.rbに追記します。
  • qst1 = { item: 'name' }では、ユーザの入力ワードに対する候補ワードをどのカラムから取得するかを指定しています。すなわち、ここのnameはUserモデルのnameカラムを指します。
  • url1がViewからController宛のパラメータを含むURLになります。このURLにtermitemという名前の2つのパラメータを含めます。termには%QUERYという値(入力ワードに該当します)、termにはnameが格納されます。そして後述しますが、これらの値をController側でparams[:term]params[:item]で受け取って使うことになります。
  • var usernames = new Bloodhound({...では、usernamesという名前でbloodoundのオブジェクトを生成しています。bloodhoundは、typeahead.jsと共に動作する候補ワード表示のためのエンジンです。正直なところ詳細をよくわかっていません。
  • jQuery( document ).ready(function( $ ) {...では、ここで指定したHTMLのid属性とclass属性を持つ要素に対してオートコンプリート機能を付与しています。具体的には、typeaheadという名前のclass属性、user_nameという名前のid属性を持つ要素にtypeaheadによるオートコンプリート機能を付与しています。また、typeaheadのオプションもここで指定しています。

続いて上記のautocomp.jsの中で各自の環境に合わせて変更必要な部分を以下にまとめておきます。

  • 1行目
  • ここの/typeaheadは後述するroutes.rbに追加するルートのパスと合わせる必要があります。
  • 3行目
  • ここのnameはUserモデルのカラム名を指します。よって、各自のモデルのカラム名に合わせてください。
  • 11行目
  • 11行目の最後、d.namenameを各自のカラム名に合わせる必要があります。
  • 15行目
  • url1も5行目で定義した名前に合わせる必要があります。
  • 20行目
  • 9行目で定義したbloodhoundのオブジェクト名にあわせます。
  • 23行目
  • #user_name.typeaheadは後ほどViewファイルのフォーム部分に付与するHTMLのid属性とclass属性です。
  • 29行目 ~ 30行目
  • 'name'はカラム名にあわせます。
  • 33行目
  • 23行目と同様に#user_name.typeaheadは後ほどViewファイルのフォーム部分に付与するHTMLのid属性とclass属性です。

上記のautocomp.jsemailのテキストフィールド用にオートコンプリートを行うコードを追加して、autocomp.jsは最終的に以下のようになります。

app/assets/javascripts/autocomp.js
var baseurl = "/typeahead?term=%QUERY"

var qst1 = { item: 'name' }
var qst2 = { item: 'email' }


var url1 = baseurl + "&" + jQuery.param(qst1)
var url2 = baseurl + "&" + jQuery.param(qst2)

var usernames = new Bloodhound({

  datumTokenizer: function(d) {return Bloodhound.tokenizers.whitespace([d.name]); },
  queryTokenizer: Bloodhound.tokenizers.whitespace,

  remote: {
            url: url1
        }
});

var useremails = new Bloodhound({

  datumTokenizer: function(d) {return Bloodhound.tokenizers.whitespace([d.email]); },
  queryTokenizer: Bloodhound.tokenizers.whitespace,

  remote: {
            url: url2
        }
});

usernames.initialize();
useremails.initialize();

  jQuery( document ).ready(function( $ ) {
      $('#user_name.typeahead').typeahead({
      hint: true,
      highlight: true,
      minLength: 1
    },
    {
      name: 'name',
      displayKey: 'name',
      source: usernames.ttAdapter()
  }).on("typeahead:selected typeahead:autocomplete", function(e, datum) {
    return $('#user_name.typeahead').val(datum.name);
  });
});

  jQuery( document ).ready(function( $ ) {
      $('#user_email.typeahead').typeahead({
      hint: true,
      highlight: true,
      minLength: 1
    },
    {
      name: 'email',
      displayKey: 'email',
      source: useremails.ttAdapter()
  }).on("typeahead:selected typeahead:autocomplete", function(e, datum) {
    return $('#user_email.typeahead').val(datum.email);
  });
});

routes.rbの修正

typeaheadはViewのフォームで入力される度にControllerとajax通信を行い、ユーザの入力ワードをControllerのメソッドに送信します。そしてControllerのメソッドはViewから送信されてきた入力ワードを元に候補ワードをViewに返します。この動作のために、Controllerのメソッドへのルートを追加する必要があります。今回はconfig/routes.rbusers_controller.rb内のtypeahead_actionへのルートを追記します。typeahead_actionへのパスとして/typeaheadを指定しています。

config/routes.rb
get  '/typeahead' => 'users#typeahead_action'

この後ここで指定したtypeahead_actionという名前のメソッドをControllerに追加します。

Controllerの修正

routes.rbに書いたようにtypeahead_actionという名前のメソッドを追加します。そしてこのメソッドの内容は以下のようにします。

app/controllers/users_controller.rb
def typeahead_action
      render json: User.select("#{params[:item]}").where(User.arel_table["#{params[:item]}".to_sym].matches("%#{params[:term]}%")).uniq
end

上記のようにjson形式の値をrenderで返すだけのメソッドです。返す値はUserモデルのActive recordで検索した結果になります。 params[:item]は候補ワードの取得先であるカラム名であり、params[:term]はユーザの入力ワードになります。 例として、typeahead_actionが受け取ったparams[:item]params[:term]がそれぞれ「test」、「name」であった場合は以下の検索結果を返すことになります。

User.select("name").where(User.arel_table[:name].matches("%test%")).uniq

これはnameカラムの値の内、「test」を含む値でユニークなものを返します。このnameカラムの検索結果をViewに返し、候補ワードとして表示することになります。上記のように検索カラム名自体もautocomp.jsからのパラメータで指定できるようにすることで、フォームに合わせた候補ワードを返すことができます。よって、params[:item]emailという値が格納されていれば、emailカラムから候補ワードを検索しその結果を返します。

なお、RailsのActive Recordのarel_tableの使い方については以下のサイトが参考になりました。ありがとうございました。

whereにSQLの文字列を渡したくない! | Qiita

Viewの修正

次にtypeaheadによるオートコンプリート機能を実装するフォームを修正します。今回は、新規Userを追加するページであるnew.html.erbnameemailのテキストフィールドにオートコンプリート機能を実装します。すなわち、nameemailのテキストフィールドのHTMLのclass属性とid属性に、上のautocomp.jsの23行目、33行目で指定した名前の属性を付与します。

テキストフィールドにclass属性、id属性を付与

以下のようにname、emailのテキストフィールドそれぞれにtypeaheadという名前のclass属性、nameのテキストフィールドにuser_nameという名前のid属性、emailのテキストフィールドにuser_emailという名前のid属性を付与します。なお、テキストフィールドにform-controlというclass属性も指定していますが、これはBootstarpのCSSを使用するために指定しているだけであり、typeaheadの動作させるために必要なわけではありません。

<div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name, class: "form-control typeahead", id: "user_name" %>
  </div>
  <div class="field">
    <%= f.label :email %><br>
    <%= f.text_field :email, class: "form-control typeahead", id: "user_email" %>
</div>

javascriptを読み込むための記述を追加

new.html.erbtypeahead.bundle.min.jsautocomp.js を読み込ませるために以下をnew.html.erbの一番上に追記しておきます。

<%= javascript_include_tag "typeahead.bundle.min" %>
    <%= javascript_include_tag "autocomp" %>

最終的にnew.html.erbは以下のようになります。

app/views/users/new.html.rb
<%= javascript_include_tag "typeahead.bundle.min" %>
<%= javascript_include_tag "autocomp" %>

<h1>New User</h1>

<%= form_for(@user) do |f| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% @user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name, class: "form-control typeahead", id: "user_name" %>
  </div>
  <div class="field">
    <%= f.label :email %><br>
    <%= f.text_field :email, class: "form-control typeahead", id: "user_email" %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

<%= link_to 'Back', users_path %>

以上でViewの修正は完了です。

動作確認

あとは各自のRailsアプリでオートコンプリートを実装したフォームに何か入力してみて候補ワードが表示されればOKです。

思うように動作しない場合の確認方法

ブラウザのデベロッパツールで確認

以下のように、ChromeやFirefox Developer Editionのデベロッパツールを使えば、javascriptでエラーが発生している場合はすぐに分かります。

chrome-tool.jpg

個人的にはjavascriptが読み込まれていないことが原因である場合が多かったです。特にapp/assets/javascripts/application.jsの中に//= require_tree .があることが原因だったりします。これについては下記が参考になりました。ありがとうございました。

Rails require_treeを排除し、アセットパイプラインで、コントローラー固有のCSS、JavaScriptを組み込む | Beautiful Ajax

ターミナルで動作確認

Railsをrails s -b 0.0.0.0で起動した場合は、以下のようにオートコンプリートを実装したフォームで入力を行うと、それと同時にtypeaheadによってControllerに入力ワードがajax通信によって送信され、その送信内容などがターミナルに表示されます。もし入力に応じてターミナルに何も表示されない場合はtypeaheadが動作していない可能性が高いです。

typeahead-console2.gif

上のgifだと見難いですが、ユーザの入力に応じて以下の様な内容がターミナルに表示されると思います。以下はNameのテキストフィールドに「us」と入力した時の表示内容です。

Started GET "/typeahead?term=us&item=name" for 192.168.33.1 at 2015-03-18 11:35:36 +0900
Processing by UsersController#typeahead_action as JSON
  Parameters: {"term"=>"us", "item"=>"name"}
  User Load (1.4ms)  SELECT DISTINCT "users"."name" FROM "users" WHERE ("users"."name" LIKE '%us%')
Completed 200 OK in 3ms (Views: 1.1ms | ActiveRecord: 1.4ms)

また、ルートが存在しない場合は以下の様にターミナルに表示されるのでわかりやすいです。

Started GET "/typeahead_action?term=te&item=name" for 192.168.33.1 at 2015-03-18 11:46:29 +0900
ActionController::RoutingError (No route matches [GET] "/typeahead_action"):

以上のように、ターミナルの表示内容を見ると動作しない原因がわかるかもしれません。

おわりに

typeahead.jsを使おうとしたら思ったように動作せず時間を取られたので次回のためにメモしました。なお、日本語対応については以下参考サイトが参考になるかもしれません。

参考サイト様

twitter typeaheadはいろいろ改良されていた | Qiita

関連記事

開発アプリ

nanolog.app

毎日の小さな出来事をなんでも記録して、ログとして残すためのライフログアプリです。