はじめに
現在友人依頼の作付計画アプリケーションの作成中です
今回はJavascriptを使用して動的なセレクトフォームを作成したので
アウトプットします✍️
間違えているところ等ございましたら、ご指摘いただけますと幸いです🙇
完成図
畑名を選択すると、畑名に関連付けられた区画名が出てくるようにしています!
イメージ的には都道府県がわかりやすく、都道府県=畑 市町村=区画 作付計画はプロフィールの出身地といったところです!下記の記事が大変分かり易かったです!
ER図
前提条件
畑(field)と区画(field_secrion)は親子関係にあり、
動的入力フォームを使用して関連づけられています
動的入力フォームについてはこちらの記事でまとめています
実装
modelのアソシエーション
畑
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を保存するのに必要な記述です!今回の動的セレクトフォームには関係ありません!
畑区画
class FieldSection < ApplicationRecord
# アソシエーション
belongs_to :field
has_many :plans
end
作付計画
class Plan < ApplicationRecord
belongs_to :user
belongs_to :field
belongs_to :field_section
end
controller記述とJSONの記述
作付計画のコントローラは特筆すべきところはありません
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データ取得のための記述
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形式に変換したデータを再格納するため以下のコードが必要です
respond_to do |format|
format.json { render json: @field_sections }
ルーティング
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画面
機能に関係のない箇所は省略しています
<%= 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の記述
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 = '区画を選択してください';
}
});
});
細かく区切って書いていきます!
const fieldSelect = document.getElementById('plan_field_id');
const fieldSectionSelect = document.getElementById('plan_field_section_id');
先ほどのデフォルトidを探して、それぞれ変数に格納します
fieldSelect.addEventListener('change', function() {
const fieldId = this.value;
// 区画選択を初期化
fieldSectionSelect.innerHTML = '';
畑名のセレクトフォームを監視します!
change
はユーザーが異なる選択肢を選んだ時にイベントがトリガーされ、
選択肢が変更される度に、fieldIdに格納されています
また、選択肢が変更されるたびに畑区画のセレクトフォームを初期化します
if (fieldId) {
:
} else {
fieldSectionSelect.length = 1;
fieldSectionSelect.children[0].text = '区画を選択してください';
}
fieldIdが存在しているかのif文です
何かしらの畑が選択されていないとelse
の部分が実行され、
区画のセレクトフォームに、区画を選択してくださいと表示されます
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の部分!!!
@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()
が必要!
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を使用してループ処理します!
※ここのループ処理などをコメントにてアドバイスいただいたので修正しました!
勉強不足でしたので、勉強し直してきます!!(理解深めてから解説追加予定です)
さいごに
アドバイスいただき修正いたしました!
本当にありがとうございます…!
勉強になりますっ!!
参考にした記事
ありがとうございました!!