内容
form_withにプルダウン選択を導入しようと思い実装していく中で、何個かトライアンドエラーを繰り返した所があったので、備忘として残しておくもの。
環境
・Ruby 2.6.5
・Rails 6.0.3
前提の状況とやりたいこと
状況
飲食店の情報を登録するshopテーブルがある。
山手線の駅データをリストとして入れたstationテーブルがある。
二つのテーブルは多対多の関係にあり、中間テーブル(shop_stationテーブル)を介してアソシエーションを組んでいる。
やりたいこと
以下の機能を実装したい。
①shopデータ作成時に、最寄り駅をプルダウンで選択し登録したい。
②最寄り駅の登録は任意で登録しなくても良い。登録できるのは最大で2駅まで。
③当然、一回のフォーム送信で一度に全ての最寄り駅を登録したい。
上記の実装手順を備忘として残していく。
※ちなみに、静的なデータを登録する場合はテーブルをいちいち作るのではなくアクティブハッシュでリストを生成することがベターという記事をいくつも読んだのですが、今回の様に多対多の関係となる場合(そしてshopが複数のstationを持つ場合)の取り扱いがあまり見えてこず、テーブルを用いて作っています。
例えばshopが一つのstationしか持たないのであれば、アクティブハッシュのidをstation_idとして登録すれば良いと思うのですが、複数持つ場合はテーブルの正規化の観点から別テーブルが好ましいよなと思い、その場合にアクティブハッシュをどう活かすのかが見えてきませんでした。
どなたかわかる方がいらっしゃったら教えていただけるとありがたいです。
さて以下で手順を書いていきます。
①テーブルデータを基にプルダウンの選択肢を作成、実装する
今回はshopコントローラのnewアクション内にフォームを実装していきます。
先にコードを抜粋し書いていきます。
Controller
before_action :set_select_lists, only: [:new]
def new
@shop = Shop.new
end
private
def set_select_lists
@stations = Station.all.map {|station| [station.name, station.id] }
end
View
.shop-wrapper
= form_with model: [@owner, @shop], html: {class: "shopform"}, local: true do |f|
-# 中略(syntaxハイライトを当てるためインデントは適当です。すいません。hamlの記法に従ってインデントしてください)
= f.select :station_ids, @stations,{},{class: "select"}
= f.select :station_ids, @stations,{},{class: "select"}
*最寄り駅を登録することで駅指定検索に表示される様になります
それぞれの説明は以下の通りです。
Controller
@stationsで今回の選択肢の基になるデータを作成しています。
ざっくり説明していくと以下の通りです。
Station.all
Stationテーブルの全データ
Station.all.map {~}
Stationテーブル全データの中の各データ(レコード)に関し、{}の通りに処理していく
Station.all.map {|station| [station.name, station.id]}
各レコードをstationとし、station.name(各レコードのnameカラムの値)をキー、station.id(idカラムの値)をバリューとするハッシュを生成する。
View
= f.select :station_ids, @stations,{},{class: "select"}
form_withでデータを送る。
f.selectでプルダウンを作成。データを送る際に:station_idsというキーで送る。選択肢は@stationsを基に作る。クラス名はselect。
コントローラーで定義したキーが**「プルダウンに表示される選択肢」、バリューが「paramsで送る値」**になります。
人が選ぶときは駅名で選んで、データ登録時はidで紐付けたいですよね。だからこの様にキーとバリューを設定した訳です。
※ちなみにクラス属性は第四引数で設定しなければならないため、空の第三引数を存在させるため間に{}が入っています。
これらを実装した図が以下の通りです。
同じプルダウンが二つ並んでいて、一つを開いている図です。確かにstation.nameが選択肢として表示されていますね。
②最寄り駅の登録を任意登録にする
先ほどの画像だと、テーブルデータでリストを作っているため、「駅を選択しない」という「選択肢」が存在しないです。
よくある「以下から選んでください」を作るため、以下の通りコントローラを書き換えました。
def set_select_lists
@stations = Station.all.map {|station| [station.name, station.id] }.unshift(["以下から選んでください", nil])
end
先ほどの@stationsに、.unshift(["以下から選んでください", nil])と追記しています。
unshiftは配列の一番最初に要素を追加するメソッドです。キーとして「以下から選んでください」という文章を、バリューにnilを渡す選択肢を追加したのです。
テーブルのデータ自体に「最寄り駅がない」というようなレコードを追加するのは中間テーブルも無駄にレコードが増えるし好ましくないと判断し、バリューをnilとする選択肢を自分で追加した形ですね。
これで無事、思い通りのプルダウンが実装できました。
③一回のフォーム送信で一度に全ての最寄り駅を登録する
ここまできたらもう登録できるだろと思ったんですけど、実際にparamsからデータを飛ばした時、以下の問題が生じました。
プルダウンが二つともstation_idsという同名のキーでデータを送っているため、2個目の値が1個目の値を上書きし、1つのみ値を送るという結果になっている。
binding.pryで見ると一目瞭然でした。
しかしこちらのやりたいこととしては複数のstation_idsを送り登録したいのです。すなわち、station_idsを配列で送りたいのです
やり方は何個かあると思うのですが、僕はviewを以下の通り書き換えました(これ多分力技なんですが、結構その後もフォームで活用できました)
= f.select :station_ids, @stations,{},{name: 'shop[station_ids][]', class: "select"}
= f.select :station_ids, @stations,{},{name: 'shop[station_ids][]', class: "select"}
先ほどの状態にname属性を追加しています。
form_withでname属性をつけると、**「どの様な形でそのparamsを送るのか?」**を指定できます。
実際にbinding.pryでparamsを見るとどの様な形でデータが送られているのかがわかります。
station_idsは **params[:shop][:station_ids]**という形で送られます。
これを配列にしたかったので、name属性で最後に[]をふしてあげることで無事配列の形で両方のデータを送ることができる様になりました。
def shop_params
params.require(:shop).permit(:name, :address, :capacity, :owner_id, :genre_id, :mark_ids,introduces_attributes: [:content, :image, :number],station_ids: [])
end
ちなみに先ほどparams[:shop][:station_ids]の形でデータが送られていると行ったのですが、だから上記の様にストロングパラメータではまず:shopをrequireして、その中のキーをpermitすることで無事データが入るわけです。
これはbinding.pryしまくる中でよく理解できました。面白かった。
終わりに
name属性で指定するやり方、結構力技感あるのでもっとスマートなやり方会ったら教えて欲しいです。
が、結構この後のフォームの各値もゴリゴリこれで実装していきました。
binding.pryでparamsがどんな形で送られているか見るのが一番勉強になりましたね。
*初学者ゆえ何かあればご指摘いただけると嬉しいです。