箱のプログラミング日記。

渋谷の自社開発企業でRails書いてます。

fields_forを使った子モデルへの複数レコード保存【cocoonが便利】

f:id:y_hakoiri:20191102121704j:plain

Railsで商品予約システムを開発中です。

あるオブジェクトをcreateする際に、一部の情報だけ別モデル(子モデル)へ保存したい時って頻繁にあると思いますが

そこに非同期(JavaScript)によるフォームの出現を組み合わせた、最高に便利なgemに出会いました。

非常に感動したので書いておきます

 

今回登場するモデルは以下の通り

  • 注文(order)
  • 商品(product)
  • 注文された商品たち(ordered_product)

 

その他ユーザーとかもありますが今回は割愛します

 

一回の注文で複数の商品が予約されることを想定して、

ordersテーブルには予約の日時やユーザー情報を格納し

ordered_productsテーブルに商品、個数、order_idを格納します

https://i.gyazo.com/6483c7658494792bf7a9265010520abb.png

 

ordered_productsテーブル

https://i.gyazo.com/fa3be6efec2ae45416af800d1bf4b505.png

こんな感じに。

product_idが全部1になってますが、これにより

一つのorderが複数のproductを注文できる関係です

 

 

fields_forでネストさせたフォームの実装 

モデル、テーブルの作成

$rails g model ordered_product

 

class CreateOrderedProducts < ActiveRecord::Migration[5.2]
 def change
  create_table :ordered_products do |t|
   t.references :order, foreign_key: true, null: false
   t.references :product, foreign_key: true, null: false
   t.integer :count, null: false
   t.timestamps
  end
 end
end
 

アソシエーションの定義

【order.rb】

class Order < ApplicationRecord

 has_many :ordered_products, dependent: :destroy

end
 
【product.rb】
class Product < ApplicationRecord
 has_many :ordered_products
end
 
【ordered_product.rb】
class OrderedProduct < ApplicationRecord
 belongs_to :order
 belongs_to :product
end
 

ストロングパラメーターに子モデルのカラムを追加

赤字部分を追加します

【orders_controller.rb】

private
def order_params
 params.require(:order).permit(:date,
              :time,
              :receiving_method,
              :receiving_store,
              :delivery_address,
              :payment,
              :voucher,
              :message,
              [ordered_products_attributes: [:order_id, :product_id, :count]]
              ).merge(user_id: current_user.id)
end

 

子モデルに保存するフォームを作成

赤字部分を追加 

【order.rb】

class Order < ApplicationRecord

 has_many :ordered_products, dependent: :destroy

 accepts_nested_attributes_for :ordered_products, allow_destroy: true

end

 

【orders_controller.rb】

def new
 @order = Order.new
 @order.ordered_products.build
end
 
 
で、あとはform_for(もしくはform_with)の中でfields_forを使えば
親モデルのオブジェクトをcreateすると同時に
子モデルに任意のレコードを保存できるが、
 
 
【views/order/new.html.haml】
= form_with(model: @order, local: true) do |f|
 = f.fields_for :ordered_products do |op|
  = f.label :product_id, '商品'
  = f.collection_select :product_id, Product.all, :id, :name, {prompt: '---'}, {class: 'select-box'}
 
これだとフォームが一つしか出てこなくて
複数の商品の選択ができない。
 
最初からフォームが複数並んでいると不恰好なので、非同期で出現させたいな
JSでイベントを発火させるのが面倒だな
と言う時に最適なgemがcocoonです
 
https://i.gyazo.com/6483c7658494792bf7a9265010520abb.png
もう一度見直すと、
「別の商品を追加」を押すとフォームが新たに出現し
「削除」を押すとそのフォームが消え、
入力された分だけ子モデルに保存をしてくれる、と言う感じです
 
これを全部gemでやってくれるんだから本当に楽です。 
 

cocoonを導入

【Gemfile】
gem 'jquery-rails'
gem 'cocoon'
 
$bundle install
$rails s
 

【application.js】

//= require jquery
//= require cocoon
 

ビューを編集

※cocoonの記述が分かりやすいよう、個人的に当てたクラス等は外しています

 

【views/order/new.html.haml】
= f.fields_for :ordered_products do |op|
 = render 'ordered_product_fields', f: op
 .links
  = link_to_add_association '+別の商品を追加', f, :ordered_products
 
フォームは部分テンプレートで実装する
  • ファイル名に規約があるらしく、_モデル名_fieldsにする必要がある
  • 「別の商品を追加」のところはlinkになっていて、直前のlinksクラスが必要
(あらかじめスタイルが当てられている模様)
 
 
【views/order/_ordered_product_fields.html.haml】
.nested-fields
 = f.label :product_id, '商品名'
 = f.collection_select :product_id, Product.all, :id, :name
 = f.label :count, '×'
 = f.number_field :count
 = link_to_remove_association '削除', f
 
部分テンプレート先にもいくつか規約あり
  • 全体をnested-fieldsクラスで囲む必要がある
  • 「削除」のところはリンクになっていて、部分テンプレート側に記述
 
 
こんな感じです。
いくつかオプションもあるらしく、公式に色々書いてありました
部分テンプレートのファイル名や親フォームのf.など「こうしなきゃいけない」みたいなのが結構あり、変える場合はオプションの定義が必要なようです
 
ちなみに、削除ボタンですが
最初の1つ目のフォームから削除ボタンがついてしまい、
最悪フォームを全部消してしまうこともできる状態になっていたので
そこは自分でjsを書いて、二番め以降のフォームの時のみ削除ボタンが出現するように改良しました。
 
 
 【views/order/_ordered_product_fields.html.haml】
削除ボタンにidを追加
.nested-fields
 = f.label :product_id, '商品名'
 = f.collection_select :product_id, Product.all, :id, :name
 = f.label :count, '×'
 = f.number_field :count
 = link_to_remove_association '削除', f, id: 'remove-btn'
 
 
【remove_btn.js】
$(document).on('turbolinks:load', function() {
 if ($('.nested-fields').length == 1) {
 $('#remove-btn').hide();
 }
});
 
これもオプションであるのかもしれませんが、
英語読むの疲れたので自分で。笑
 
 
以下参考

www.write-ahead-log.net

 

 ------------------------------------- 

 

私がこれまでの学習で読んだ本をこちらにまとめています。※随時更新

www.y-hakopro.com

 

転職の時に使って良かったサイト・おすすめのサイトを紹介しています

www.y-hakopro.com