[Rails]インクリメンタルサーチの実装

# はじめに
今回は以前実装したインクリメンタルサーチの実装の振り返りをします。


# インクリメンタルサーチとは

文字の入力の都度、自動的に検索が行われる検索機能です。
JavaScriptのAjaxを用いて、実装を行います。
以下のGIFでは、正と打つと正確というタグが候補として即座に表示されます。


<a href="https://gyazo.com/0ecc4456001c754e7e3c345c680d9e13"><img src="https://i.gyazo.com/0ecc4456001c754e7e3c345c680d9e13.gif" alt="Image from Gyazo" width="584"/></a>

# 実装

それでは実際の実装についてみていきます。

```tag.js
if (location.pathname.match("/new")||location.pathname.match("/edit")||location.pathname.match("/practices")){

  document.addEventListener("DOMContentLoaded", () => {
    const inputElement = document.getElementById("practices_ptag_name");
    inputElement.addEventListener("keyup", () => {
      let keyword = document.getElementById("practices_ptag_name").value;
      if(keyword.includes(',')){
        keyword=keyword.substring(keyword.lastIndexOf(',')+1,keyword.length);
      }

      const XHR = new XMLHttpRequest();
      XHR.open("GET", `/practices/ptaglist/?keyword=${keyword}`, true);
      XHR.responseType = "json";
      XHR.send();

      XHR.onload=()=>{
        const searchResult = document.getElementById("search-result");
        searchResult.innerHTML = "";
        if (XHR.response) {
          const tagName = XHR.response.keyword;
          console.log(tagName)
          tagName.forEach((ptag) => {
            //2語目以降、重複キーワードは表示しない
            let inputValue=document.getElementById("practices_ptag_name").value;
            if(inputValue.includes(',')){
              console.log(`ptag=${ptag}`);
              console.log(`inputValue=${inputValue}`);
              let flag=false;
              words=inputValue.split(",");
              words.forEach((word)=>{
                if(ptag.name==word){
                    flag=true;
                    return true;
                }
              });
              if(flag==true){return true};
            }
            ////////////
            const childElement = document.createElement("div");
            childElement.setAttribute("class", "child");
            childElement.setAttribute("id", ptag.id);
            childElement.innerHTML = ptag.name;
            searchResult.appendChild(childElement);
            const clickElement = document.getElementById(ptag.id);
            
            clickElement.addEventListener("click", () => {
              let tagForm= document.getElementById("practices_ptag_name") ;
              tagForm.value=tagForm.value.substring(tagForm.value.lastIndexOf(",")+1,tagForm.value.lastIndexOf(",")-tagForm.length);
              tagForm.value=tagForm.value.concat(clickElement.innerText);
              
              clickElement.remove();
            });
          });
        };
      };
    });
  });
};

```

かなりの量があるので1つづつみていきます。

###前半戦

まずは、前半戦として入力情報をコントローラへ送信する部分を見てみましょう

```tag.js
document.addEventListener("DOMContentLoaded", () => {
    const inputElement = document.getElementById("practices_ptag_name");
    inputElement.addEventListener("keyup", () => {
      let keyword = document.getElementById("practices_ptag_name").value;
      if(keyword.includes(',')){
        keyword=keyword.substring(keyword.lastIndexOf(',')+1,keyword.length);
      }

      const XHR = new XMLHttpRequest();
      XHR.open("GET", `/practices/ptaglist/?keyword=${keyword}`, true);
      XHR.responseType = "json";
      XHR.send();

　　　//省略
```

まず、document.getElementByIdで入力フォームを指定します。その入力フォームにkeyupというイベントがあるごとにjs側へ情報を送信します。keyupとは、キーを押して話したときに発生します。

入力した後（キーを離した時）に起きることは、単に、コントローラへ情報を送信しますよということをします。このへんは特に説明は要らないかなと思います。
1点着目としたら、以下の部分ですね

```js
 if(keyword.includes(',')){
        keyword=keyword.substring(keyword.lastIndexOf(',')+1,keyword.length);
      }
```

実は今回、","で区切ることで複数タグに対応できるようにしています。そのため、入力された情報を「〇〇,正・・」と受け取るのではなく、最後の「,」から先の文字のみほしいわけです（正〜の部分）。そのための加工処理をこおｋで行っています。

#後半戦（コントローラから受け取った後）

後半戦は、コントローラからレスポンスを受け取った後の処理を見ていきます。

```js
　//省略
 XHR.onload=()=>{
        const searchResult = document.getElementById("search-result");
        searchResult.innerHTML = "";
        if (XHR.response) {
          const tagName = XHR.response.keyword;
          console.log(tagName)
          tagName.forEach((ptag) => {
            //2語目以降、重複キーワードは表示しない
            let inputValue=document.getElementById("practices_ptag_name").value;
            if(inputValue.includes(',')){
              console.log(`ptag=${ptag}`);
              console.log(`inputValue=${inputValue}`);
              let flag=false;
              words=inputValue.split(",");
              words.forEach((word)=>{
                if(ptag.name==word){
                    flag=true;
                    return true;
                }
              });
              if(flag==true){return true};
            }
            ////////////
            const childElement = document.createElement("div");
            childElement.setAttribute("class", "child");
            childElement.setAttribute("id", ptag.id);
            childElement.innerHTML = ptag.name;
            searchResult.appendChild(childElement);
            const clickElement = document.getElementById(ptag.id);
            
            clickElement.addEventListener("click", () => {
              let tagForm= document.getElementById("practices_ptag_name") ;
              tagForm.value=tagForm.value.substring(tagForm.value.lastIndexOf(",")+1,tagForm.value.lastIndexOf(",")-tagForm.length);
              tagForm.value=tagForm.value.concat(clickElement.innerText);
              
              clickElement.remove();
            });
          });
        };
      };
```

XHR.onloadはレスポンスの受け取りに成功した際に機能します。
レスポンスを受けとった後に行うことは、”レスポンスに含まれる入力値から予測されるタグ候補の情報をフォームの下に挿入”をします。
この処理は簡単ですので見ればわかると思います。ポイントは、すでに入力した単語が候補として現れないようにする処理です。例えば、「正確、正」とうったときに、候補として正確は出したくありません。そのために、以下のような処理を行います。

```js
  tagName.forEach((ptag) => {
            //2語目以降、重複キーワードは表示しない
            let inputValue=document.getElementById("practices_ptag_name").value;
            if(inputValue.includes(',')){
              console.log(`ptag=${ptag}`);
              console.log(`inputValue=${inputValue}`);
              let flag=false;
              words=inputValue.split(",");
              words.forEach((word)=>{
                if(ptag.name==word){
                    flag=true;
                    return true;
                }
              });
              if(flag==true){return true};
            }
            ////////////
```

ここで行っていることは、　tagNameという候補ワードの配列1個づつみて、それが、すでにある入力値と重ねっていれば、flagをtrueとします。trueの場合は、return true とすることでその後の処理、すなわち、候補ワードの表示をしません。結果として、既存入力値に対する候補は表示しないわけです。

#参考：コントローラ

一応、コントローラのコードも記載しておきます

```controller.rb
  def ptaglist
    return nil if params[:keyword] == ''

    ptag = Ptag.where(['name LIKE ?', "%#{params[:keyword]}%"])
    render json: { keyword: ptag }
  end
```

#おわりに

インクリメンタルサーチにはかなり苦戦しましたが、こうして振り返ってみるととてもシンプルな原理で動いているな〜と感じました！言語化って大事！！！