##結論 親モデルにaccepts_nested_attributes_forを追加する
###実証環境
・Cloud9 Ubuntu Server
・Rails 6.0.0
・Ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]
###前提条件
次のような一対多のリレーションを持つParentモデルとKidモデルがあるとします。
※1:混乱を避けるため、「Parentって父親?母親?」とかは考えないものとします。
※2:モデル名に不規則変化の名詞を用いるとややこしいので、childではなくkidを用いています。
class Parent < ApplicationRecord
has_many :kids, dependent: :destroy
end
class Kid < ApplicationRecord
belongs_to :parent
end
###ネストしたフォームの実装
親モデルのデータ保存時に子モデルのデータも同時に保存するため、accepts_nested_attributes_for
をParentモデルに追加します。
class Parent < ApplicationRecord
has_many :kids, dependent: :destroy
accepts_nested_attributes_for :kids
end
これによって関連付けたデータを1つのフォームで登録できるネストしたフォームを使用できるようになります。
次に、親モデルのParentsコントローラーにて、フォームのページから送られてくるparamsを受け取る空のインスタンスを作ります。
その際、関連付けられた子モデルの空のインスタンスも作成します。
また、子モデルのpramsを受け取るために、ストロングパラメーターに
kids_attributes: [:name, :age, :toy]
を渡しています。
class ParentsController < ApplicationController
#(中略)
def new
@parent = Parent.new
@parent.kids.build #子モデルの空のインスタンスを作成
end
def create
@parent = Parent.new(parent_params)
if @parent.save
redirect_to root_url
else
render :new
end
end
#(中略)
private
def parent_params
#子モデルのパラメーターを受け取れるようにする
params.require(:parent).permit(
:name, :age, kids_attributes: [:name, :age, :toy]
)
end
end
<div class="container">
<div class="col-sm-10 col-sm-offset-1">
<h1 class="text-center">親登録</h1>
<%= form_with(model: @parent, local: true) do |f| %>
<div class="field form-group">
<%= f.label :name %>
<%= f.text_field :name, class: "form-control" %>
</div>
<div class="field form-group">
<%= f.label :age %>
<%= f.number_field :age, class: "form-control" %>
</div>
<!-- 子モデルのデータを受け取るためのネストされたフォーム -->
<%= f.fields_for :kids do |kf| %>
<h1 class="text-center">子登録</h1>
<div class="field form-group">
<%= kf.label :name %>
<%= kf.text_field :name, class: "form-control" %>
</div>
<div class="field form-group">
<%= kf.label :age %>
<%= kf.number_field :age, class: "form-control" %>
</div>
<div class="field form-group">
<%= kf.label :toy %>
<%= kf.text_field :toy, class: "form-control" %>
</div>
<% end %>
<div class="field form-group">
<%= f.submit "上記内容で登録する", class: "btn btn-primary btn-lg btn-block" %>
</div>
<% end %>
</div>
</div>
Bootstrapのクラスが入っており見にくいですが、上記のf.fields_for
が子モデルのデータを受け取るフォームになります。
###データ更新(update)時に新たな関連付けられたデータを保存できるようにする
子モデル用の入力フォームですが、空のインスタンスを複数個作成することで、view自体のフォームは増やさずに入力欄を増やすことができます。
例えば、Parentsコントローラーを次のように変更することで、新規登録時には1組の子モデル入力フォーム、更新時には2組の子モデル入力フォームを表示させることができます。
class ParentsController < ApplicationController
#(中略)
def new
@parent = Parent.new
@parent.kids.build #子モデルの空のインスタンスを作成
end
def create
@parent = Parent.new(parent_params)
if @parent.save
redirect_to root_url
else
render :new
end
end
#(中略)
def edit
@parent = Parent.find(params[:id])
@parent.kids.build #子モデルの空のインスタンスを作成
end
def update
@parent = Parent.find(params[:id])
if @parent.update(parent_params)
redirect_to root_url
else
render :edit
end
end
def destroy
@parent = Parent.find(params[:id])
@parent.destroy
redirect_to root_url
end
private
def parent_params
params.require(:parent).permit(
:name, :age, kids_attributes: [:name, :age, :toy]
)
end
end
editアクションでも子モデルの空のインスタンスを作成することで、更新(update)時に「登録されている子モデルのデータ」+1個の入力フォームが作成されます。
これにより、JSなどで動的に追加するよりも簡便に入力フォームを増やすことができます。(個数が限定されてしまうのがネックですが...)
追加されるフォームをさらに増やしたい場合は、生成する空のインスタンスの数を
@parent.kids.build
から
n.times { @parent.kids.build }
とすることで、n個の入力フォームを生成できます。
###入力されなかった場合の空データを保存されないようにする
しかし、このままでは追加されたフォームに入力されなかった場合に空のデータが保存されてしまいます。
それを防ぐためにはParentモデルに追加したaccepts_nested_attributes_for
の第2引数に次のようなProcを渡します。
class Parent < ApplicationRecord
has_many :kids, dependent: :destroy
accepts_nested_attributes_for :kids, reject_if: lambda {|attributes| attributes['name'].blank?}
end
上記の場合、子モデル入力フォームのnameが空の場合は他の属性(ここでは、age、toy)が入力されていても全て保存されません。
nameまたはageが入力されていない場合に登録させたくない場合は次のように変更します。
class Parent < ApplicationRecord
has_many :kids, dependent: :destroy
accepts_nested_attributes_for :kids, reject_if: lambda {|attributes| attributes['name'].blank? || attributes['age'].blank?}
end
上記に加え、子モデルへのバリデーションも変更すれば、保存される値を自由に制御できます。
この他、関連付けた子モデルのデータのみを削除したい場合などもあると思いますが、詳しくはRailsガイドの「Action View フォームヘルパー」がとても参考になります。
###あとがき
現在Ruby on Rails学習中の初学者・タツロンです。
自作アプリ作成中に数日間に渡って詰まった機能に関して投稿させていただきました。
間違っている点やアドバイス等ございましたらコメント頂けば幸いです。