[インクリメンタルサーチ]Rails+JavaScript(jQuery)でタイムレスな検索を

## 記事で扱う検索機能
こんな感じの検索機能です。
今回は、shopsというテーブルからnameというカラムを抽出してビューに表示していきます。
※店舗情報を複数登録済み。そこから任意の店舗を任意のカラムで検索します。

![edc53a4c67fcde418466da618219084f.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503967/1d114bb9-991c-acda-3aa6-c7ef8d490341.gif)

- 非同期で行う
- 文字を打つごとに検索を行い、描画する
- 検索結果をクリックで該当のページにリンクする

おそらくですが、初学者のプログラミングにおける知識レベルや常識レベルって経験者の方が考えるそれよりもずっと低い水準にあると思うんです。
なので、自身にできる範囲内で「そんな事まで説明する必要あるの？」という事も書いて行こうかと思います。

## 開発環境

mac
Ruby 2.5.1
Rails 5.2.3
jQuery

## 必要なファイル
まず、実装に必要となるファイルを以下にまとめます。

- 検索を行うアクションを定義した**コントローラー**の準備

```ruby:app/controllers/shops_controller.rb
 def search
 end
```

- 上記アクションに対応する**json.jbuilder**の準備

```ruby:views/shops/search.json.jbuilder
#上記で作成したコントローラー及びアクションに対応するようデイレクトリ関係とファイル名に注意
#この場合、views/shops(コントローラー名に対応)/search(アクション名に対応).json.jbuilder
```

- 上記アクションに対する**ルーティング**の準備

```ruby:config/route.rb
resources :shops do #shopsコントローラー内の
 collection do
  get 'search'      #searchアクション
 end
end
```

- 検索フォームと結果のを表示する**ビュー**ファイルの準備

```ruby:app/views/shops/index.html.haml
#今回は、shops一覧画面に検索を表示します。
#hamlを使って書いていきますが、通常のhtmlでもslimでも構いません。
```

- **js**ファイルの準備

```ruby:app/assets/javascripts/search.js
#.jsで終わっていれば名前はなんでも構いません
```

以上、5つのファイルを使って検索機能を実装していきます。

## どう動くの？

検索処理の流れは以下の通りです。

#### 検索フォームで起こったイベントを.jsファイルで受け取る。

○○.html.haml(○○.html.erb)　→　○○.js

#### 受け取った情報をjson形式でコントローラへ流す。

○○.js　→　○○_controller

#### コントローラーでDBから必要な情報を抽出し、その情報を.js内でも使えるようjbuilderで変換。

○○_controller　→　○○.json.jbuilder

#### 再度js側に送り、htmlへの描画を行う。

○○.json.jbuilder　→　○○.js　→　○○.html.haml


処理の流れが分かっていないと、エラーが起きた際に対処できず1時間も2時間も無駄にすることになってしまいます。
これから記載していく実際のコードには可能な限り説明を入れますので、ぜひ上記の処理の流れを頭に入れた状態で読み進めてみて下さい。

では、始めていきましょう。

## ルーティングを行う
今回は例として、**shops_controller**内に**search**アクションを設定します。

```ruby:app/controllers/shops_controller.rb
 def search
 end
```

```ruby:config/route.rb
resources :shops do #shopsコントローラー内の
 collection do
  get 'search'      #searchアクション
 end
end
```

## 検索フォームと結果表示欄の準備
```ruby:app/views/shops/index.html.haml
.search-field
 .fas.fa-search  #虫眼鏡のfontawesomeを使用しています。
 .shop_search
   = f.text_field :text, placeholder: "店舗・住所で検索", id: "shop_search"  #テキスト入力欄を設置。
 #shop_search--result  ←コメントアウトではなくshop_search--resultというid名です。検索結果を表示。
   .shop-search-list
```
## 検索フォームに入力された情報を受け取り、受け取った情報をコントローラへ送る

```ruby:app/assets/javascripts/search.js
$("#shop_search").on("keyup", function(){
  let input = $("#shop_search").val();
  $.ajax({
   type: 'GET',
   url: '/shops/search',
   data: {keyword: input},
   dataType: 'json'
  })
```

まず、検索フォーム（hamlファイル内のf.text_field）に入力された情報をjsファイルで受け取ります。
┗検索フォームにつけたid名を指定し、文字入力の際にキーボードから指を話した瞬間にkeyupで情報を取得。

ajax以下でその情報を
・どのような方法で
・どこに
・何を
・どのような状態で
送るかを指定します。

この場合、GETメソッドでshopsコントーラ内のsearchアクションにinputをjson方式で送るということになるかと思います。


## コントローラーでアクションの設定、DBとのやりとりを記載

```ruby:app/controllers/shops_controller.rb
  def search
    return nil if params[:keyword] == ""
    @shops = Shop.where('name LIKE ? OR location LIKE ?', "%#{params[:keyword]}%", "%#{params[:keyword]}%").limit(10)
    respond_to do |format|
      format.html
      format.json
    end
  end
```

今回私は、shopsテーブル作成の際にname（店舗名）カラムとlocation（所在地）カラムを設定していますので、この2情報で検索できるように設定していますが、
例えば店舗名のみで検索する場合は、

```ruby:app/controllers/shops_controller.rb
  def search
    return nil if params[:keyword] == ""
    @shops = Shop.where('name LIKE ?, "%#{params[:keyword]}%").limit(10)
    respond_to do |format|
      format.html
      format.json
    end
  end
```
でOKです。

アクション内の**params[:keyword]**は先のjsファイル内ajax以下のdata:{keyword: input}から来ています。
分かりづらいかもしれませんが、検索欄に入力した文字をjsファイル内ではinputとして扱い、コントローラファイル内ではkeywordとして扱っているということです。
検索欄に何も入力されていない場合（２行目""）はnilを返しています。

また、3行目ではshopsテーブルではそのkeyword（検索欄に入力された文字）を基に、DBから情報を引っ張り出します。
今回はparams[:keyword]の量端を**%**で囲って部分一致検索を行う仕様です。
他の検索方法については、この記事を参考にしてみて下さい。

・[Rails - LIKE句を使った文字のあいまい検索（特定の文字を含む語句を曖昧検索したい場合）](https://qiita.com/seri1234/items/765423c2c46ca4114da0)
私もこの記事を参考にさせていただきました。ありがとうございます。

そして、今回入力された文字データはjson形式でコントローラへ入って来ている為、jbuilderへと移ります。

## json.jbuilderで情報変換
```ruby:search.json.jbuilder
json.array! @shops do |shop|
  json.name           shop.name
  json.location       shop.location
end
```

コントローラー内でDBから情報を抽出しましたがこのデータをjson形式に変換する必要があります。
それをjbuilderで行うのです。

## htmlファイルへの検索結果描画

まず先ほどのjsファイルajax以下にコントローラーでの処理が終わり、戻ってきた際の処理を追記します。

```ruby:app/assets/javascripts/search.js
  $("#shop_search").on("keyup", function(){
    let input = $("#shop_search").val();
    $.ajax({
      type: 'GET',
      url: '/shops/search',
      data: {keyword: input},
      dataType: 'json'
    })
    .done(function(shops){
      $("#shop_search--result").empty();
      if (shops.length !== 0) {
        shops.forEach(function(shop){
          addShop(shop);
        });
      } 
      else if (input.length == 0){
        return false;
      } else {
        addNoShop();
      }
    });
  });
```

.done以下がその処理です。

そしてそれぞれaddShop、addNoShopとしたものをhtmlへ描画します。

以下がjsファイル全文です。

```ruby:app/assets/javascripts/search.js
$(function(){
  function addShop(shop) {
    let html = `
      <a href="/shops/${shop.id} class="shop_search-list">
        <div>${shop.name} - ${shop.location}</div>
      </a>
      `;
      $("#shop_search--result").append(html);
  };
  function addNoShop(){
    let html =`ショップがありません`
    $("#shop_search--result").append(html);
  };
  $("#shop_search").on("keyup", function(){
    let input = $("#shop_search").val();
    $.ajax({
      type: 'GET',
      url: '/shops/search',
      data: {keyword: input},
      dataType: 'json'
    })
    .done(function(shops){
      $("#shop_search--result").empty();

      if (shops.length !== 0) {
        shops.forEach(function(shop){
          addShop(shop);
        });
      } 
      else if (input.length == 0){
        return false;
      } else {
        addNoShop();
      }
    })
  });
});
```

let htmlとして描画する際の定型を指定します。
今回は検索結果をクリックするとその詳細ページにリンクする形で記載してみました。

hamlファイル内の検索結果を表示する箇所(#shop_search--result)にappendを使用して作成した定型文(html)を挿入します。

これで非同期の検索機能（インクリメンタルサーチ）の完成です。

##やってみて
当内容はスクールで学んだ内容に自身で挑戦してみて、理解を深めながら作成したものです。
学習当初は半端な理解で出来ればいいや位の気持ちで進めていましたが、いざ１から自分の力でやるとなると手が止まってしまう事が多々ありました。反省です。

ファイルが変わるごとに、どこがどのように対応しているのかを把握する事がポイントかと思います。
また、どんな機能でもそうですが、今回の場合はconsole.log等デバッグしながら書き進めていくことを強くお勧めします。