非同期通信で入力値を拾って、サーバーから欲しいデータを出力する方法

#背景
私は共同開発でRailsを用いて野球のスコアブックを作成しております。
事前登録された選手のデータについて、背番号を入力したら選手の学年と名前が連動して表示される機能が求められたのでそれの作り方を記載していきます。

分かりにくいと思うので、
具体例を出すと人事給与システムで社員番号を入力したら、会社名と名前などが紐づいて自動で表示され、かつ登録時にデータが紐づくようにしたいという感じです。

#完成イメージ
下記の形になります。
[![Image from Gyazo](https://i.gyazo.com/3f5f6554ad0079700814fa1daaccbce3.gif)](https://gyazo.com/3f5f6554ad0079700814fa1daaccbce3)

#欲しい機能を叶える為の考え方
機能に必要な技術などを選定する意味でもまず手順を考えることにしました。

1. 背番号のフォームを入力したらJavascriptにて値を取得
2. 取得したデータをパラメータとしてサーバー側に送付
3. 送付されたデータで選手テーブルデータを抽出してjson形式でクライアントに返却
4. 返却されたデータの中身（学年、名前）を各フォームに挿入

一言で説明すると、
値を取ってきてデータを抽出して、当てはまるものがあれば、必要な箇所に挿入を行うものです。

#実装方法
事前準備→考え方の手順の流れで解説を実施していきます。

#### 事前準備
データ抽出用のアクションの作成を実施します。
batting_orderコントローラーファイルの中に新規にsearch_memberアクションを作成します。

```batting_orders_controller.rb
    def create
    end
    
    def search_member
    end
```
新規アクション作成に伴いルーティングの設定を行います。

```routes.rb
Rails.application.routes.draw do
    resources :batting_orders
    collection do
      get 'search_member'
    end
  end
end
```
#### 1. 背番号のフォームを入力したらJavascriptにて値を取得

```search_member.js
function searchMember() {
  //背番号を入力する要素、データを挿入するHTML要素を全て取得、取得したHTML要素の数も取得
  const uniformNumber = document.querySelectorAll(".input-game-uniform-number");
  const memberGrade = document.querySelectorAll(".input-game-grade");
  const memberId = document.querySelectorAll(".input-member-id");
  const fullName = document.querySelectorAll(".input-game-full-name");
  const uniformNumberLength = uniformNumber.length;

  for(let i=0; i<=uniformNumberLength - 1; i++){
    //特定の背番号を入力するフォームに入力があった場合、非同期処理を実行し、入力値を取得
    uniformNumber[i].addEventListener("keyup", (e) => {
      const formResult = uniformNumber[i];
      const formData = Math.floor(formResult.value);
    });
  };
}
window.addEventListener("load", searchMember);
```
##### 補足情報
__querySelectorAll()__
カッコ内に当てはまるHTML要素を全て取得。クラス名、ID名など指定可能です
querySelectorAllで変数aに格納したとして、
a[0]で取得したHTML要素の最初の要素、a[1]で２番目の要素を取得など...
n番目のHTML要素をa[n-1]で指定することも可能です。

__変数名.addEventListener("keyup", (e) =>{処理})__
キーを押し込んで、離したタイミングで処理が実行されるイベントハンドラー
今回ではfor文で全ての背番号入力フォームに対してイベントが発火されるように設定しています。


#### 2. 取得したデータをパラメータとしてサーバー側に送付

```search_member.js
uniformNumber[i].addEventListener("keyup", (e) => {
  const formResult = uniformNumber[i];
  const formData = Math.floor(formResult.value);
  //HTTP リクエストを送るには、 XMLHttpRequest オブジェクトを作成し、URLを開いてリクエストを送信する必要があります。
  const XHR = new XMLHttpRequest();
  XHR.open("GET", '/games/search_member?keyword=' + encodeURIComponent(formData), true);
  XHR.responseType = "json";
  XHR.send(null);
  });
```
##### 補足情報
__GETパラメータ__
URLの末尾に？以降に記述された値。
パラメータをつける際は?をつけ、
keyとvalueを設定してサーバー側に送付。
今回はkeyをkeyword、valueを対象の背番号に入力された文字を値としています。

なお、text型の１フォーム分しかデータを送付しないのでGETパラメータに設定してます。
複数フォーム分のデータを送る際はPOSTパラメータにします。

__encodeURIComponent__
 URI (Uniform Resource Identifier) 構成要素を特定の文字を UTF-8 文字エンコーディングで表された 1 個から 4 個のエスケープシーケンスに置き換えることでエンコードしますとのことです。
要するに、文字を機械で読み取れる形に暗号化してくれる関数。
分かりやすいものだと改行を文字\nに変換するイメージ。

#### 3. 送付されたデータで選手テーブルデータを抽出してjson形式でクライアントに返却

```batting_orders_controller.rb
def search_member
    target = Member.find_by(uniform_number: params[:keyword], team_id: current_team.id)
    render json:{ member: target }
end
```
##### 補足情報
非同期でデータを表示する機能の代表的な例だと下記のようなインクリメンタルサーチがメジャーで参考資料もたくさん見つかりますのでそれを参考にしました。
Test.where('title LIKE(?)', "%#{search}%")
今回は、データを貰って、完全一致した一意の値が欲しいので、find_byメソッドを使用してます。おそらくtarget = {id: 1, uniform_number: 2}のような形でデータが格納されているかと思います。


### 4. 返却されたデータの中身（学年、名前）を各フォームに挿入
```search_member.js
XHR.onload = () =>{
  if (XHR.status != 200) {
    alert(`Error ${XHR.status}: ${XHR.statusText}`);
    return null;
  }
  //3でmemberのkeyで値を渡されているのでそれをjavascript上のmemberと言う変数に格納。その後各カラムを変数に格納。
  const member = XHR.response.member;
  const gradeForm = memberGrade[i];
  const memberIdForm = memberId[i];
  const fullNameForm = fullName[i];

  //３でmemberのデータが抽出出来なかった場合はundefindが返ってくるので条件分岐
  if(member === undefined || member === null){
    gradeForm.value = "";
    memberIdForm.value = "";
    fullNameForm.value = "";
  }else{
    const grade = member.grade;
    const memberNumber = member.id;
    //選手登録時に姓名と名前を分けて登録していたのでフルネームにして格納。
    const name = member.first_name + " " + member.last_name;
    gradeForm.value = grade;
    memberIdForm.value = memberNumber;
    fullNameForm.value = name;
  }
}
```
##### 補足情報
__memberGrade[i]などのiについて__
要素数[i]番目の背番号入力フォームでイベントを起こしているので要素数[i]番目の学年、名前、ID番号を格納するフォームに挿入する必要がある。その為、数値が格納された変数iで位置を指定してます。

__返却されたデータについて__
検証ツールで挙動を確認した結果、合致したデータがない場合は各変数にundefindが格納されることがわかりました。そこで変数memberがundefindかnullの場合とそうでない時とで分岐条件をかけました。

#おわりに
最後まで見てくださりありがとうございます。

特定のフォーム上のデータを拾って、サーバーに送り、データを抽出。
その後、クライアントに抽出された値を返し、何らかの操作を行う。
これを非同期で行えたことで、実装のレシピの幅が大きく向上出来たのではないでしょうか。

また、言語やフレームワークが異なってもJavaScriptの部分は使えるので、
もっと上手く扱えるように勉強していきたいです。

ご意見ご質問などありましたら記載いただきますようお願いします。

###JavaScript(全文）
```search_member.js

function searchMember() {
  const uniformNumber = document.querySelectorAll(".input-game-uniform-number");
  const memberGrade = document.querySelectorAll(".input-game-grade");
  const memberId = document.querySelectorAll(".input-member-id");
  const fullName = document.querySelectorAll(".input-game-full-name");
  const uniformNumberLength = uniformNumber.length;

  for(let i=0; i<=uniformNumberLength - 1; i++){
    uniformNumber[i].addEventListener("keyup", (e) => {
      const formResult = uniformNumber[i];
      const formData = Math.floor(formResult.value);
      // const formData = new FormData(formResult);
      const XHR = new XMLHttpRequest();
      XHR.open("GET", '/games/search_member?keyword=' + encodeURIComponent(formData), true);
      XHR.responseType = "json";
      XHR.send(null);
      XHR.onload = () =>{
        if (XHR.status != 200) {
          alert(`Error ${XHR.status}: ${XHR.statusText}`);
          return null;
        }
        const member = XHR.response.member;
        const gradeForm = memberGrade[i];
        const memberIdForm = memberId[i];
        const fullNameForm = fullName[i];
        if(member === undefined || member === null){
          gradeForm.value = "";
          memberIdForm.value = "";
          fullNameForm.value = "";
        }else{
          const grade = member.grade;
          const memberNumber = member.id;
          const name = member.first_name + " " + member.last_name;
          gradeForm.value = grade;
          memberIdForm.value = memberNumber;
          fullNameForm.value = name;
        }
      }
    });
  };
}

window.addEventListener("load", searchMember);
```