表がクロスした箇所の登録・更新機能の作成方法

#作成の背景
現在、野球のスコアブックを記録するアプリの作成を実施しています。
野球の１回、１打席目の人の成績を登録したいという場合に、
スコアブックにて１回と１打席がクロスした箇所をクリックすれば登録フォームが表示できる機能があればと考えミニアプリを作成して挙動の確認を実施することにしました。

#ミニアプリの仕様
生徒がどの科目で何点とったかが分かるマトリックス表を作成しました。
表のクロスした箇所をクリックすると点数を記入できるようにしていきます。
また、既存に入力した項目に関しては入力値の更新ができるようにします。

###DB設計
[![Image from Gyazo](https://i.gyazo.com/2b34ed22d24988def54a2b340a9386c6.png)](https://gyazo.com/2b34ed22d24988def54a2b340a9386c6)

###完成イメージ図
[![Image from Gyazo](https://i.gyazo.com/1dfd3ddda0970e324dd1fc44ca4db2f6.gif)](https://gyazo.com/1dfd3ddda0970e324dd1fc44ca4db2f6)


#作成方法
1. ルーティングの作成
2. コントローラの作成
3. ビューの作成
4. Javascriptの作成

### 1. ルーティングの作成
```routes.rb
Rails.application.routes.draw do
  root to: 'cross_answers#index' 
  resources :students, only: [:index, :new, :create]
  resources :lessons, only: [:index, :new, :create]
  resources :cross_answers, only: [:index, :create, :update, :destroy]
end
```

### 2. コントローラの作成
```cross_answers_controller.rb
class CrossAnswersController < ApplicationController
  def index
    @lessons = Lesson.all
    @students = Student.all
    @cross_answers = CrossAnswer.all
    パラメータが表示されていれば指定されたクロスアンサーのデータを表示
    if params[:lesson_id] && params[:student_id]
      set_cross_answer
    else
    @cross_answer = CrossAnswer.new
    end

  end
  def create
    @cross_answer = CrossAnswer.new(cross_answer_params)
    if @cross_answer.save
      redirect_to root_path
    else
      render :index
    end
  end

  def update
    @cross_answer = CrossAnswer.find(params[:id])
    if @cross_answer.update(cross_answer_params)
      redirect_to root_path
    else
      render :index
    end
  end

  private
  def cross_answer_params
    params.require(:cross_answer).permit(:lesson_id, :student_id, :writing, :mark)
  end

  def set_cross_answer
    @cross_answer = CrossAnswer.find_by(lesson_id: params[:lesson_id], student_id: params[:student_id])
  end

end
```
#### 補足
注目していただきたいのは、indexアクションの時にパラメータが送られてきた場合と処理を分岐している点です。
パラメータがない時は登録、ある時は更新の処理を実行するためにインスタンス変数の格納する値を変えております。更新時はstudent_id,lesson_idをパラメータで送付し、そのIDが合致した一意のデータを変数に格納してビューで利用してます。

### 3. ビューの作成
```cross_answers/index.html
<div id="cross-matrix">
  <table class="matrix">
    <thead>
      //マトリックス表の科目名を上端に表示
      <tr class="matrix">
        <td class="matrix"></td>
        <% @lessons.each do |lesson| %>
          <td class="matrix">
            <%= lesson.name %>
          </td>
        <% end %>
      </tr>
    <thead>
    <tbody>
      //生徒名分項目を出力
      <% @students.each do |student| %>
        <tr class="matrix">
          //生徒名を左端に表示
          <th class="matrix">
            <%= student.name %>
          </th>
          //科目項目分列を表示
          <% @lessons.each do |lesson| %>
            <td class="grade-info matrix">
             //非表示の生徒テーブル、科目テーブルのIDを格納
              <div class="hidden lesson-num">
                <%= lesson.id%>
              </div>
              <div class="hidden student-num">
                <%= student.id %>
              </div>
              //生徒IDと科目のIDを持つクロスアンサーテーブルがあれば、テストの点数を２つ表示
              <% if @cross_answers.find_by(lesson_id: lesson.id, student_id: student.id).present? %>
                <% cross_answer = @cross_answers.find_by(lesson_id: lesson.id, student_id: student.id) %>
                //点数を表示されている時には、登録フォームではなく編集フォームを立ち上げる。
                <%= link_to cross_answers_path(lesson_id: cross_answer.lesson_id, student_id: cross_answer.student_id) do %>
                  <%= cross_answer.writing %>
                  <%= cross_answer.mark %>
                <% end %>
              <% end %>
            </td>
          <% end %>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>
//パラメータに生徒IDと科目IDがない場合、クロス箇所をクリックしたら登録フォームが立ち上がる
<% unless params[:lesson_id] && params[:student_id] %>
  <div id="mask" class="hidden">
    <section id="modal" class="hidden">
      <div id="close">
        閉じる
      </div>
      <%= form_with model: @cross_answer, local: true do |f| %>
        <%= f.text_field :lesson_id, class:"hidden",id:"lesson_id-input"%>
        <%= f.text_field :student_id, class:"hidden", id:"student_id-input" %>
        <div>
          <label>筆記点数</label> 
          <%= f.text_field :writing %>
        </div>
        <div>
          <label>マーク点数</label>
          <%= f.text_field :mark %>
        </div>
        <%= f.submit %>
      <% end %>
    </section>
  </div>
<% else %>
  //パラメータに生徒ID、科目IDがあれば、編集フォームが表示されっぱなしになる。
  <div id="edit-mask">
    <section id="modal">
      //閉じるをクリックでクロスアンサーのパラメータなしのページに遷移
      <%= link_to cross_answers_path do %>
        <div id="edit-close">
          閉じる
        </div>
      <% end %>
      <%= form_with model: @cross_answer, url:cross_answer_path(@cross_answer.id), local: true do |f| %>
        <%= f.hidden_field :id %>
        <%= f.text_field :lesson_id, class:"hidden",id:"lesson_id-input"%>
        <%= f.text_field :student_id, class:"hidden", id:"student_id-input" %>
        <div>
          <label>筆記点数</label> 
          <%= f.text_field :writing %>
        </div>
        <div>
          <label>マーク点数</label>
          <%= f.text_field :mark %>
        </div>
        <%= f.submit %>
      <% end %>
    </section>
  </div> 
<% end %>
```
#### 補足
__マトリックスの表の作り方について__
tableタグで表を作成しております。
１行目はeachメソッドを使い科目名分列を増やし、科目名を表示してます。
2行目以降はeachメソッドを使い生徒名分行を増やします。
1列目に生徒名を表示し、それ以降の列は再度eachメソッドを使い科目名分列を増やします。
eachメソッドの中にeachメソッドを格納するネスト構造になっております。
これでマトリックス表を作成することができましす。

__表の格納データについて__
表には4つのデータを格納できるようにしております。
・非表示の科目ID
・非表示の生徒ID
上記2点のIDを持つクロスアンサーテーブルのデータがあれば表示される
・マーク点数
・筆記点数

クロスした場所をクリックしたら表の生徒IDと科目IDをコピーして、登録フォームの非表示になっている科目IDと生徒IDを入力するテキストボックスにペーストするようにJavaScriptで実装しております。

__登録と更新の切替について__
データがない場合はマトリックスの空白部分をクリックしてJavaScriptで登録フォームを表示
データがある場合は、数値をクリックしたら生徒IDと科目IDのパラメータをコントローラに送り、その２つのIDに合致した一意のクロスアンサーデータをインスタンス変数として格納し、クライアントに返す。また、ビューの設定において、パラメータがある場合は、更新用のフォームが常時立ち上がるようにしています。

__クラス名について__
マトリックス表で科目と生徒のクロスした箇所には全て共通のクラス名を付与している。これはJavaScriptにおいてクリックした箇所でイベントを発火できるようにする為である。

__ID名について__
JavaScriptで使用したいHTML要素にID名をつける。

### 4. Javascriptの作成
```modal.js
function modalAddJoin(){
  //フォームを入力するHTML要素に共通のクラス名をつけているので全要素を取得
  let cross_info = document.querySelectorAll(".grade-info");
  let info_num = cross_info.length;
  //クリックした箇所でイベントが発生するようにする。
  for(let i=0; i<=info_num - 1; i++){
    cross_info[i].onclick = function(){
      //生徒ID、科目IDを取得して、登録フォームの入力項目に転記
      let lessonId = this.children[0].innerHTML.trim();
      let studentId = this.children[1].innerHTML.trim();
      const modal = document.getElementById("modal");
      const mask = document.getElementById("mask");
      const close = document.getElementById("close");
      const studentInput = document.getElementById("student_id-input");
      const lessonInput = document.getElementById("lesson_id-input");
      studentInput.value = Math.floor(studentId);
      lessonInput.value = Math.floor(lessonId);
      //非表示のクラス名を除去
      modal.classList.remove("hidden");
      mask.classList.remove("hidden")
      //閉じるを押した時、非表示のクラス名を追記
      close.onclick = function(){
        modal.classList.add("hidden");
        mask.classList.add("hidden");
      };
    }
  }
}
window.addEventListener("load", modalAddJoin);
```
#今後の展望について
マトリックス表のクロスした箇所をクリックして登録と表示ができたことにより、本番環境の野球のスコアブックでも活用できそうだと感じました。
唯、１点だけ懸念点があれば、パラメータを送付して登録と更新のフォームの切替を実施しているので、余計なページ遷移が発生することが挙げられます。これを改良して非同期通信で登録や更新してその問題を解消していきたいと考えております。
近日中に改良した記事を書けるように挑戦してみます。

#最後に
ここまで読んで下さりありがとうございます。模索しながら機能を実装しておりますので効率が悪い部分があるかと存じます。もしご意見やご指摘がございましたらよろしくお願いいたします。