Railsで商品予約システムを開発中です。
あるオブジェクトをcreateする際に、一部の情報だけ別モデル(子モデル)へ保存したい時って頻繁にあると思いますが
そこに非同期(JavaScript)によるフォームの出現を組み合わせた、最高に便利なgemに出会いました。
非常に感動したので書いておきます
今回登場するモデルは以下の通り
- 注文(order)
- 商品(product)
- 注文された商品たち(ordered_product)
その他ユーザーとかもありますが今回は割愛します
一回の注文で複数の商品が予約されることを想定して、
ordersテーブルには予約の日時やユーザー情報を格納し
ordered_productsテーブルに商品、個数、order_idを格納します
ordered_productsテーブル
こんな感じに。
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です
もう一度見直すと、
「別の商品を追加」を押すとフォームが新たに出現し
「削除」を押すとそのフォームが消え、
入力された分だけ子モデルに保存をしてくれる、と言う感じです
これを全部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();
}
});
これもオプションであるのかもしれませんが、
英語読むの疲れたので自分で。笑
以下参考