検索機能で検索boxの下に候補を出す方法

#0 はじめに
　今回は以下の写真のように、ユーザーが検索する時の助けになるように、boxの下に候補を出す方法を記述します。
　もっといい方法があるとは思いますが、今の私の理解力と技術ではこれがベストです。

<img width="700" alt="" src="https://i.gyazo.com/ed9060f3ff00cffbead476716de7b95a.png">

バージョンです

```
　version ruby 2.5.1p57
          Rails 5.2.4.1
```
  
# １　実装の流れ

### a) controllerで候補に出すデータをインスタンス変数に入れる
### b) viewにinputタグ（type:hidden）を記述。その中に１の変数を記述
### c) 検索boxに入力されるたびに発火させ処理させる
### d) 検索boxに入力された値をもとに正規表現を作成
### e) インスタンス変数の値をeachで取り出して正規表現とマッチするか調べる
### f) マッチしたものだけ関数を使って要素を作り、検索boxにappendする


# ２　コード・解説
### a) controllerで候補に出すデータをインスタンス変数に入れる
jsサイドにデータを渡すためのコードです。
今回は「new.html.haml」 「edit.html.haml」以外に検索機能を実装します。productsコントローラーにbeforeアクションを設定しました。@wordに商品名を入れ、@id_boxにproduct.idを入れます。

```products_controller.rb
class ProductsController < ApplicationController
 before_action :set_product_name, except: [:new, :edit]
 ---省略---
 def set_product_name
   @product_for_search = Product.all
   @wordbox = []
   @id_box = []
   @product_for_search.each do |product|
     @id_box << product.id
　　　@word_box << product.name
   end
  end
end
```

### b) viewにinputタグ（type:hidden）を記述。その中に１の変数を記述
　jsが発火した時にここから商品名及び、商品idを取得できるようにします。
　一番下の#result-wordの下にliで候補を表示します


```index.html.haml
= form_with(url: products_searches_path, local: true, method: :get, class: "search-form") do |form|
　.search-group
　　= form.text_field :keyword, placeholder: "商品を検索する", class: "main-header__search-box", id: "_products_searches_keyword"
　　= form.label :keyword, for: "search-btn", class: "search-label" do
　　　= form.submit "検索", class: "search-btn", id: "search-btn", style: "display: none"
　　　= image_tag 'icon-search 1.png',size: "30x25",class: "main-header__search-img"
　　%input{name: "search-word-list", type: "hidden", value: @wordbox, class: 'search_word_list' }　　←この行を入れる
　　%input{name: "search-id-list", type: "hidden", value: @id_box, class: 'search_id_list' }　　　　　←この行を入れる
　%ul#result-word　←この下に候補用の要素を追加する
```

### c) 検索boxに入力されるたびに発火させ処理させる

jsファイル完全版

```search.js
$(document).on('turbolinks:load', function(){
  var searchWordList = $('.search_word_list').val();


  function appendList(word, number) {    
    let item = $(`
    <li class="list result-list">
      <a href = "/products/${number}" class="search-word-list">
        <p>${word}</p>
    `);
    $("#result-word").append(item);
  }

  function editElement(element) {
    if (element != ""){
      let result = "^" + element;
      return result;
    }else{
      let result = "$^";
      return result;
    }
  }
  
  $("#_products_searches_keyword").on("keyup", function() {
    let input = $("#_products_searches_keyword").val();
    if (input==""){
      $("#result-word").empty();
    }else {
      let inputs = input.split(" ");
      let newInputs = inputs.map(editElement);
      let reg = RegExp(newInputs.join("|"));


      $.each( JSON.parse(searchWordList), function(i, word) {
        var searchIdList = $('.search_id_list').val();
        searchIdList =JSON.parse(searchIdList)
        if (word.match(reg)) {
          appendList(word,searchIdList[i]);
        }
      });
    };
  });
});
```

**search.jsの詳細コード**

```search.js
 var searchWordList = $('.search_word_list').val(); ⬅️①

 $("#_products_searches_keyword").on("keyup", function() {　⬅️②
    let input = $("#_products_searches_keyword").val();
    if (input==""){
      $("#result-word").empty();
    }else {
      let inputs = input.split(" ");
      let newInputs = inputs.map(editElement);
      let reg = RegExp(newInputs.join("|"));


      ---省略---
      });
    };
  });
```

①　ページが読み込まれた時に商品の名前、idを取得する
②　↓入力されるたびに候補が出て欲しいので、keyupを使って発火させる。
　　↓発火すると検索boxに入れられた値を取得する
　　↓何も入力されていなければ候補用の要素を削除する
　　↓入力された検索ワードが複数ある場合はスペースで区切り、配列(inputs)にする
　　↓配列(inputs)をmapで新しい配列にする　d)に飛ぶ
　　↓正規表現用の変数を用意し、先ほど ^ マークを付けた配列を"|"(「または」を意味する)を間に入れて合体させる。
  

### d) 検索boxに入力された値をもとに正規表現を作成

```search.js
 function editElement(element) {
    if (element != ""){
      let result = "^" + element;
      return result;
    }else{
      let result = "$^";
      return result;
    }
  }
```
 inputで分けられた配列の中身があるときだけ検索ワードの先頭に ^ マークをつける。「test ␣」のようにスペースが空いている場合は「"^test", "^"」というふうに配列が作成されるので、すべての商品名がヒットしてしまう。「""」の時は ^ マークは付けないようにする。　b)に戻る。

### e) インスタンス変数の値をeachで取り出して正規表現とマッチするか調べる

```search.js
 $.each( JSON.parse(searchWordList), function(i, word) {
   var searchIdList = $('.search_id_list').val();
   searchIdList =JSON.parse(searchIdList)
   if (word.match(reg)) {
   appendList(word,searchIdList[i]);
   }
 });
```
　一行目のJSON.parseはsearchWordListをobjectに変換するためのメソッド
　jsのeachはobjectに対して処理を行うようでこの記述がないとエラーが表示される。（理解仕切れていません）

>Uncaught TypeError: Cannot use 'in' operator to search for 'length' in ["aa", "test", "選択してくださいテスト", "test-sample", "服"]

商品のidにも同じように記述する
はじめは商品のidを渡さずに、eachのindexを使ってリンクのURLを作成していたが、本番環境でエラーが出たので、書き換えました。

### f) e)のeachでマッチしたものだけ、関数を使って要素を作り、検索boxにappendする

```search.js
 function appendList(word, number) {    
   let item = $(`
   <li class="list result-list">
    <a href = "/products/${number}" class="search-word-list">
     <p>${word}</p>
  `);
  $("#result-word").append(item);
  }
```

候補をクリックするとその商品のページに移動するためリンクを作成しています。その時にproduct.idが必要なめviewから取得しました。


　

# ３　最後に

今回のポイントは
・商品のデータをビューからjsに渡す
・検索ワードを入力している途中でスペースが入った時にすべての商品名がヒットしてしまうので、スペースには ^ マークを付けない
・JSON.parseでobjectに変換する
・検索ワードを正規表現にして該当するものだけヒットするようにする


以上です。
最後までご覧いただきありがとうございました。