ステップ1・既存の store の一覧から選択できるよう変更
モデルに accepts_nested_attributes_for :一緒に保存したいアソシエーションの名前
を追加します。
diff
1[models]
2
3 class Product < ApplicationRecord
4 belongs_to :user
5 has_many :product_stores
6 has_many :stores, through: :product_stores
7
8+ accepts_nested_attributes_for :product_stores, reject_if: :all_blank
9 end
model に合わせて view を修正します。
先ほどの accepts_nested_attributes_for
に応じた fields_for
を定義します。
diff
1[views]
2
3 <%= form_with model: @product, local: true do |f| %>
4 <%= render 'layouts/error_messages', model: f.object %>
5
6 <div class="form-group">
7- <%= f.label :product_name %>
8- <%= f.text_field :product_name, class: 'form-control' %>
9+ <%= f.label :name %>
10+ <%= f.text_field :name, class: 'form-control' %>
11 </div>
12
13+ <%= f.fields_for :product_stores do |product_stores_form| %>
14 <div class="form-group">
15- <%= f.label :store_name %>
16- <%= f.text_field :store_name, class: 'form-control' %>
17+ <%= product_stores_form.label :store %>
18+ <%= product_stores_form.select :store_id, @store_list, {}, { class: 'form-control' } %>
19+ <%# @store_list は後程コントローラーにて定義 ↑ %>
20 </div>
21 <div class="form-group">
22- <%= f.label :regular_price %>
23- <%= f.text_field :regular_price, class: 'form-control' %>
24+ <%= product_stores_form.label :regular_price %>
25+ <%= product_stores_form.text_field :regular_price, class: 'form-control' %>
26 </div>
27 <div class="form-group">
28- <%= f.label :discounted_price %>
29- <%= f.text_field :discounted_price, class: 'form-control' %>
30+ <%= product_stores_form.label :discounted_price %>
31+ <%= product_stores_form.text_field :discounted_price, class: 'form-control' %>
32 </div>
33+ <% end %>
34 <%= f.submit 'submit', class: 'btn btn-primary' %>
35 <% end %>
view に表示するための @store_list を作成します。
diff
1[controllers]
2
3 class ProductsController < ApplicationController
4+ before_action :set_store_list, only: %i[new create edit update]
5
6 private
7
8+ # セレクトボックスの選択肢
9+ def set_store_list
10+ @store_list = [
11+ ['選択してください', nil],
12+ *Store.all.pluck(:name, :id),
13+ ]
14+ end
view から送られたデータを正しく受け取れるよう、 controller を修正します。
diff
1[controllers]
2
3 def product_params
4- params.require(:product).permit(:product_name, :store_name, :regular_price, :discounted_price)
5+ params.require(:product).permit(:name,
6+ product_stores_attributes: [
7+ :id,
8+ :store_id,
9+ :regular_price,
10+ :discounted_price,
11+ ])
12 end
データの新規作成時は product に紐づく product_stores が存在しないため、 fields_for 部分に何も出力されません。それでは困るので、入力フィールドが画面に表示されるように、コントローラで空データを build します。
diff
1[controllers]
2
3 def new
4 @product = Product.new
5
6+ # 表示用に3件ぶんの空データを作成
7+ 3.times do
8+ @product.product_stores.build
9+ end
10 end
ここまでで、おそらく既存の store から選択して登録する処理はうまくいくと思います。
ステップ2・新規 store を登録できるよう機能追加
ここから少しトリッキーになります。
まずはセレクトボックスの選択肢に「(新規 store を登録)」を追加します。
diff
1[controllers]
2
3 # セレクトボックスの選択肢
4 def set_store_list
5 @store_list = [
6 ['選択してください', nil],
7 *Store.all.pluck(:name, :id),
8+ ['(新規 store を登録)', 'new-store'],
9 ]
10 end
新規 store 名を入力するための attribute を定義します。
diff
1[models]
2
3 class ProductStore < ApplicationRecord
4+ attr_reader :new_store_name
5
6 belongs_to :product
7 belongs_to :store
8
9+ def new_store_name=(name)
10+ build_store(id: nil, name: name) if name.present?
11+
12+ @new_store_name = name
13+ end
14 end
画面に入力フィールドを追加します。
diff
1[views]
2
3 <%= f.fields_for :product_stores do |product_stores_form| %>
4 <div class="form-group">
5 <%= product_stores_form.label :store %>
6 <%= product_stores_form.select :store_id, @store_list, {}, { class: 'form-control' } %>
7+ <%= product_stores_form.text_field :new_store_name,
8+ class: 'form-control',
9+ placeholder: '新しい store の名前を入力してください',
10+ disabled: product_stores_form.object.new_store_name.blank?,
11+ hidden: product_stores_form.object.new_store_name.blank? %>
12 </div>
javascript で入力フィールドの表示・非表示を切り替える機能を実装します。
diff
1[javascript]
2
3+ document.querySelectorAll('select').forEach((select) => {
4+ select.addEventListener('change', () => {
5+ if (select.value === 'new-store') {
6+ select.nextElementSibling.disabled = false;
7+ select.nextElementSibling.hidden = false;
8+ } else {
9+ select.nextElementSibling.disabled = true;
10+ select.nextElementSibling.hidden = true;
11+ }
12+ });
13+ });
画面パラメータを受け取れるよう permit を変更します。
diff
1[controllers]
2
3 def product_params
4 params.require(:product).permit(:name,
5 product_stores_attributes: [
6 :id,
7 :store_id,
8+ :new_store_name,
9 :regular_price,
10 :discounted_price,
11 ])
12 end
ここまでで、画面から新たな store が登録できるようになっているはずです。
ステップ3・一覧画面の変更
特に解説することはないです。
html は適当なので table タグなどを使って良い感じに整えてください。
erb
1<%= @product.name %>
2
3 店舗名 特別価格 通常価格
4<% @product.product_stores.each do |product_store| %>
5 <%= product_store.store.name %>
6 <%= product_store.regular_price %> 円
7 <%= product_store.discounted_price %> 円
8<% end %>
ステップ4・登録画面で動的に入力欄を追加する
いまの登録画面では、固定で3件分の store の入力欄が表示され、それ以上増やすことができません。
回答がかなり長くなってしまったので詳細な解説は省きますが、gem cocoon を導入すればその辺りも解決できると思うので、ドキュメントを確認してみてください。
https://github.com/nathanvda/cocoon