fields_forを使って、関連先をチェックボックスで紐付けていたのですが、関連を切りたい場合に困ってしまいました。チェックボックスを外して保存してもうまく削除されずゴミのデータが残ってしまったりしたためです。
これを解決するための方法を見つけたので、メモとして残しておきます。
サンプル
では、サンプルのコードを書きます。
Before
Controller
class OrdersController < ApplicationController
before_action :set_order, only: [:show, :edit, :update, :destroy]
def new
@order = Order.new
Item.all.each do |item|
@order.order_items.build(item: item)
end
end
def edit
Item.all.each do |item|
@order.order_items.build(item: item) unless @order.items.exists?(item)
end
end
private
def set_order
@order = Order.find(params[:id])
end
def order_params
params.require(:order).permit(:title, order_items_attributes: [:id, :item_id])
end
end
Model
Orderモデルは複数のItemを持ちます。
class Order < ActiveRecord::Base
has_many :order_items
has_many :items, through: :order_items
accepts_nested_attributes_for :items
end
Orderに持たれるItemモデルです。
class Item < ActiveRecord::Base
has_many :order_items
end
OrderとItemを取り持つ中間テーブルを設けます。
class OrderItem < ActiveRecord::Base
belongs_to :order
belongs_to :item
end
View
viewにはfields_forの部分を追加しました。
check_boxにはitemのidをvalueとして持たせます。
チェックが入らない場合はhiddenに設定された0が渡ってきます。
<%= form_for(@order) do |f| %>
<% if @order.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:</h2>
<ul>
<% @order.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<div class="field">
<label>アイテム</label><br>
<%= f.fields_for :order_items do |fo| %>
<label>
<%= fo.hidden_field :id %>
<%= fo.check_box :item_id, {checked: fo.object.persisted?}, fo.object.item_id %>
<%= fo.object.item.name %>
</label>
<% end%>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
この状態でPOSTすると、チェックが入ってない場合はitem_idが0の状態で中間テーブルに登録されてしまいます。行自体は削除されません。
After
(追記)mark_for_destructionを使うパターンの場合、リレーション先のバリデーションで、presense: true
になっていた場合、item_idに0が渡った状態だと弾かれてエラーになってしまうことがわかったので、やっぱり_destroyを使うように変更しました。
model
変更はOrderモデルです。accepts_nested_attributes_forにallow_destroy
を追加します。
class Order < ActiveRecord::Base
has_many :order_items
has_many :items, through: :order_items
accepts_nested_attributes_for :items, allow_destroy: true
end
View
Viewでは、check_boxで_destroyをつけます。チェックがついているときの値を0、ついてない場合を1とすることで、チェックがない場合は削除フラグが付くというふうにしておきます。
<%= form_for(@order) do |f| %>
<% if @order.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:</h2>
<ul>
<% @order.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<div class="field">
<label>アイテム</label><br>
<%= f.fields_for :order_items do |fo| %>
<label>
<%= fo.hidden_field :id %>
<%= fo.hidden_field :item_id %>
<%= fo.check_box :_destroy, {checked: fo.object.persisted?}, 0, 1 %>
<%= fo.object.item.name %>
</label>
<% end%>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
こうすることで、関連は綺麗に削除されました。