0
0

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

Last updated at Posted at 2024-08-22

はじめに

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

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

完成図

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

Image from Gyazo

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

ER図

前提条件

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

Image from Gyazo

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

実装

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

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を使用します)

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形式に変換します

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

簡単に解説 🌱
  • サーバー側では、データを標準的なフォーマットでクライアントに送信している
  • クライアント側では、サーバーから返ってきたレスポンスを文字列として受け取っている
    →文字列からJavascript内で操作アクセスしやすい形式に変換するためresponse.json()が必要!
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を使用してループ処理します!
※ここのループ処理などをコメントにてアドバイスいただいたので修正しました!
勉強不足でしたので、勉強し直してきます!!(理解深めてから解説追加予定です)

さいごに

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

参考にした記事

ありがとうございました!!

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0