RailsでJavascriptを使用して動的セレクトフォームの作成

# はじめに

現在友人依頼の作付計画アプリケーションの作成中です
今回はJavascriptを使用して動的なセレクトフォームを作成したので
アウトプットします✍️

間違えているところ等ございましたら、ご指摘いただけますと幸いです🙇

# 完成図
畑名を選択すると、畑名に関連付けられた区画名が出てくるようにしています！

[![Image from Gyazo](https://i.gyazo.com/83145a3b2e1498d16452466f0721c0c9.gif)](https://gyazo.com/83145a3b2e1498d16452466f0721c0c9)

イメージ的には都道府県がわかりやすく、都道府県=畑　市町村=区画　作付計画はプロフィールの出身地といったところです！下記の記事が大変分かり易かったです！

https://kimuraysp.hatenablog.com/entry/2016/06/17/004922


# ER図

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/bdbafc40-c391-6204-37ef-a585f794e302.png">

# 前提条件
畑(field)と区画(field_secrion)は親子関係にあり、
動的入力フォームを使用して関連づけられています

[![Image from Gyazo](https://i.gyazo.com/9d603c9f7e409d1e19240bd2226993df.gif)](https://gyazo.com/9d603c9f7e409d1e19240bd2226993df)

動的入力フォームについてはこちらの記事でまとめています

https://qiita.com/3rarara/items/e62fef62a58a7f6fdaac

# 実装

### modelのアソシエーション

畑
```field.rb
class Field < ApplicationRecord

  # アソシエーション
  belongs_to :user
  has_many :field_sections, dependent: :destroy
  has_many :plans, dependent: :destroy

  # 子モデル（field_sections）の属性を受入れ、更新や削除を許可する
  accepts_nested_attributes_for :field_sections, allow_destroy: true
end
```

:::note warn
※`accepts_nested_attributes_for`の部分はfield/new画面でfield_sectionを保存するのに必要な記述です！今回の動的セレクトフォームには関係ありません！
:::

畑区画
```field_section.rb
class FieldSection < ApplicationRecord

  # アソシエーション
  belongs_to :field
  has_many :plans
end
```

作付計画
```plan.rb
class Plan < ApplicationRecord
  belongs_to :user
  belongs_to :field
  belongs_to :field_section
end
```

### controller記述とJSONの記述

作付計画のコントローラは特筆すべきところはありません

```plans_controller.rb
class Public::PlansController < ApplicationController

  def new
    @plan = Plan.new
    @fields = current_user.fields.all
  end

  def create
    @plan = current_user.plans.new(plans_params)
    @plan.save
    redirect_to plan_path(@plan)
  end

  private
  
  def plans_params
    params.require(:plan).permit(:year, :title, :planting_method, :start_date, :end_date, :note, :field_id, :field_section_id, :crop_id)
  end

end
```

JSONデータ取得のための記述

```fields_controller.rb
class Public::FieldsController < ApplicationController
:

  def field_section_list
    @field_sections = FieldSection.where(field_id: params[:field_id])
    respond_to do |format|
      format.json { render json: @field_sections }
    end
  end
:
end
```

`@field_sections = FieldSection.where(field_id: params[:field_id])`は
`params[:field_id]`で指定されたfield_idを持つすべてのFieldSectionのレコードを`@field_sections`に格納しています

例えば、A畑に関連づけられた「A区画、B区画、C区画」です

しかしこのままだと、JSON形式でないため、Javascript上でデータが受け取れません
そのため、JSON形式に変換したデータを再格納するため以下のコードが必要です

```fields_controller.rb
    respond_to do |format|
      format.json { render json: @field_sections }
```

### ルーティング

```routes.rb
Rails.application.routes.draw do
:
  scope module: :public do
:
    resources :fields do
      collection do
        get "field_section_list"
      end
    end
  end
end
```

`field_section_list`アクションのルーティングを定義することで、
fields_controller.rbのfield_section_listアクションが呼びだし可能になります
つまり`fields/field_section_list`というURLから先ほどコントローラで定義したJSON形式のデータが受け取り可能となります

### view画面

機能に関係のない箇所は省略しています

```plans/new.html.erb
<%= javascript_pack_tag 'plans' %>

<%= form_with model: @plan, local: true do |f| %>
  <%= f.label :field_id, "畑名" %>
  <%= f.select :field_id, options_from_collection_for_select(@fields, :id, :name), { include_blank: '畑名を選択してください'} %>

  <%= f.label :field_section_id, "区画名" %>
  <%= f.select :field_section_id, [], {} %>

  <%= f.submit '登録' %>
<% end %>
```

`<%= f.select :field_id, options_from_collection_for_select(@fields, :id, :name), { include_blank: '畑名を選択してください'} %>`では、ログイン中のユーザーが登録している畑をすべて表示し選択できるようにしています
valueはfield_idで、ユーザーが見るviewにはnameが表示されます

畑名と区画名のセレクトボックスにはidを手動付与していませんが、明示的にidを付与しても問題ありません
(記述が長くなるので今回はデフォルトで付与されているidを使用します)

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/5dd91b8f-ac28-f98a-afd3-1cfadbcd5ab4.png">


### Javascriptの記述

```plans.js
document.addEventListener('turbolinks:load', function() {
  const fieldSelect = document.getElementById('plan_field_id');
  const fieldSectionSelect = document.getElementById('plan_field_section_id');

  fieldSelect.addEventListener('change', function() {
    const fieldId = this.value;

    // 区画選択を初期化
    fieldSectionSelect.innerHTML = '';

    if (fieldId) {
      // 区画情報を取得
      fetch(`/fields/field_section_list?field_id=${fieldId}`)
        .then(response => response.json())
        .then(data => {
          fieldSectionSelect.length = data.length + 1;
          fieldSectionSelect.children[0].text = '区画名を選択してください';
          data.forEach((field_section, i) => Object.assign(fieldSectionSelect.children[i + 1],
            {
              text: field_section.name,
              value: field_section.id,
            }
          ));
        });
    } else {
      fieldSectionSelect.length = 1;
      fieldSectionSelect.children[0].text = '区画を選択してください';
    }
  });
});
```

細かく区切って書いていきます！

```plan.js
  const fieldSelect = document.getElementById('plan_field_id');
  const fieldSectionSelect = document.getElementById('plan_field_section_id');
```
先ほどのデフォルトidを探して、それぞれ変数に格納します

```plan.js
  fieldSelect.addEventListener('change', function() {
    const fieldId = this.value;

    // 区画選択を初期化
    fieldSectionSelect.innerHTML = '';
```
畑名のセレクトフォームを監視します！
`change`はユーザーが異なる選択肢を選んだ時にイベントがトリガーされ、
選択肢が変更される度に、fieldIdに格納されています
また、選択肢が変更されるたびに畑区画のセレクトフォームを初期化します

```plan.js
    if (fieldId) {
    :
    } else {
      fieldSectionSelect.length = 1;
      fieldSectionSelect.children[0].text = '区画を選択してください';
    }
```

fieldIdが存在しているかのif文です
何かしらの畑が選択されていないと`else`の部分が実行され、
区画のセレクトフォームに、区画を選択してくださいと表示されます

```plan.js
    if (fieldId) {
      // 区画情報を取得
      fetch(`/fields/field_section_list?field_id=${fieldId}`)
        .then(response => response.json())
        .then(data => {
          fieldSectionSelect.length = data.length + 1;
          fieldSectionSelect.children[0].text = '区画名を選択してください';
          data.forEach((field_section, i) => Object.assign(fieldSectionSelect.children[i + 1],
            {
              text: field_section.name,
              value: field_section.id,
            }
          ));
        });
```

`featch`関数では、指定したURLのHTTPリクエストを送信するための関数です
ただ先ほど定義したURL`/fields/field_section_list`だけでは、field_idがないため、
関連する`＠field_sections`を取得することができません

そのため、`field_id=${fieldId}`のようにリクエストURLにクエリパラメータを追加することで、区画のリストを取得することができます↓コントローラのparamsの部分!!!

```fiels_controller.rb
@field_sections = FieldSection.where(field_id: params[:field_id]) 
# paramsの部分がないと関連する区画データを格納できない！！！
```

`.then(response => response.json())`
fetch関数がサーバーからのレスポンスを受け取ると、このthenメソッドが呼ばれます
response.json()は、サーバーからのレスポンスをJSON形式に変換します

::: note
ここで私は疑問に思いました(なぜ今まで疑問に思わなかったのか)
コントローラ側(サーバー側)でもJSON形式に変換したのに
JS側(クライアント側)でもJSON形式に変換するの…？

<details><summary>簡単に解説 🌱</summary>

- サーバー側では、データを標準的なフォーマットでクライアントに送信している
- クライアント側では、サーバーから返ってきたレスポンスを文字列として受け取っている
→文字列からJavascript内で操作アクセスしやすい形式に変換するため`response.json()`が必要！
</details>
:::

```plan.js
fieldSectionSelect.length = data.length + 1;
fieldSectionSelect.children[0].text = '区画名を選択してください';
data.forEach((field_section, i) => Object.assign(fieldSectionSelect.children[i + 1],
  {
    text: field_section.name,
    value: field_section.id,
  }
```
あとはレスポンスが帰ってきたデータにオプションなどを定義したあと、
forEachを使用してループ処理します！
※ここのループ処理などをコメントにてアドバイスいただいたので修正しました！
勉強不足でしたので、勉強し直してきます！！(理解深めてから解説追加予定です)




# さいごに
アドバイスいただき修正いたしました！
本当にありがとうございます…！
勉強になりますっ！！

# 参考にした記事
ありがとうございました！！

https://kimuraysp.hatenablog.com/entry/2016/06/17/004922

https://qiita.com/i__kobe/items/3974d9ec78dbf082a5ef

https://zenn.dev/redheadchloe/articles/fe144fd90e30d0