どんもー、@snskOgataです。
今回はRails開発におけるヘルパーメソッドform_with/form_forについて深掘りしていこうと思います。
特にform_withに
・モデルが渡されているときと渡されていないときでの振る舞いの違い
・渡された変数が空の場合と既存のものであった時の場合
をそれぞれ話していこうと思います。
と、その前にform_withとform_forの違いについて触れておきます。
1. form_withとform_forの違いについて
form_withは元々Rails5.1以前のバージョンで使い分けられていたform_forとform_tagをどちらも同様に扱えるようにしたもの、らしいです。
それまでは、関連するモデルが用意されている場合はform_forでモデルを渡してやり、関連するモデルがない場合はurlをform_tagに渡して、それぞれフォームを作っていました。
関連するモデルがない場合→form_tag
<%= form_tag users_path do # user_pathというURIを渡している %>
<%= text_field_tag :email %>
<%= submit_tag %>
<% end %>
関連するモデルがある場合→form_for
<%= form_for @user do |form| # @userというインスタンス変数を渡している %>
<%= form.text_field :email # 受け取ったモデルから作ったformを利用 %>
<%= form.submit %>
<% end %>
####form_with
これらがform_withで一緒くたに扱えるようになったみたいです。
<%= form_with url: users_path do |form| # urlを渡している %>
<%= form.text_field :email %>
<%= form.submit %>
<% end %>
<%= form_with model: @user do |form| # modelを渡している %>
<%= form.text_field :email %>
<%= form.submit %>
<% end %>
Railsが5.1以上であればform_withを使うことが推奨されているようです。
参考記事:【Rails 5】(新) form_with と (旧) form_tag, form_for の違い
form_withとform_for/form_tagで大きな違いはないので、ここからは推奨されているform_withを例に説明していきます。
2. form_withの動作について
form_withは渡されたものによって、行うHTTPメソッドとアクションをそれぞれ判断してくれます。
2.1 form_with url: url_path
urlが渡されたのであれば、そのpathに対してPOSTメソッドを行います。
<%= form_with url: users_path do |form| # urlを渡している %>
<%= form.text_field :email %>
<%= form.submit %>
<% end %>
users_pathに対してPOSTを行い、このコードであれば
params[:email]
のような形でtext_fieldに入力された値を取得することができます。
これによりストロングパラメータでも
params.permit(:email)
のように設定することが可能です。
2.2 form_with model: @model
この場合は、モデルに入っているものが①新しく作られたものの場合、と②既存のものを呼び出した場合、で処理が変わってきます
①新しく作られたものが渡された場合
newアクションから渡ってきたものがこれに当てはまります。
def new
@user = User.new
end
<%= form_with model: @user do |form| # modelを渡している %>
<%= form.text_field :email %>
<%= form.submit %>
<% end %>
Railsは渡されたモデル**(@user)の中身が空**であることから、createメソッドを呼び出すことを判断します。
def create
User.create(user_params)
end
private
# ストロングパラメータ
def user_params
paramas.requie(:user).permit(:email)
end
このとき気を付けなければいけないのが、paramsの中身。
先ほどのurlを渡したformであればparams[:email]で取得できたのですが、今回のものはモデルを渡したことにより、
params[:user][:email]
のように階層構造になっていることに注意していください!
それによりストロングパラメータの設定では**.require(:user)**のように一度仲介を挟む必要が出てきます。
②既存のものが渡された場合
editアクションから渡ってきた物がこれに当てはまります。
def edit
@user = User.find(params[:id]) # DBから既存のものを取得
end
<%= form_with model: @user do |form| # modelを渡している %>
<%= form.text_field :email %>
<%= form.submit %>
<% end %>
実のところnew.html.erbとedit.html.erbのフォーム部分は全くの一緒なんです!
これによりリファクタリングを行え、フォーム部分をパーシャル(部分テンプレート)化できたりします。
さて、Railは渡されたモデル**(@user)の中身がある**ことからupdateメソッドを呼び出すことを判断します。
def update
User.find(params[:id]).update(user_params)
end
# 以下略
書くのは省略していますが、ここでもparamsは階層構造になっており:emailを取り出すにはparams[:user][:email]のように取得する必要があります。
ここまでだと単に紛らわしくなるだけでは...と思うかもしれませんが、form_withの真価はここからもう一歩先にあります!
2.3 form_with model: [@modelA, @modelB]
railsではルーティングによってページが階層構造にすることができます。
resources :tweets do
resources :comments only: [:index, :create]
end
これにより、tweets/:id/commentsのようにtweetのひとつに紐ついたcomments群、のようなページ設計を行うことが可能です。
このときのコメント作成の際に使えるのが、form_withに複数モデルを渡してやる、という方法です。
def index
# モデルを2つ用意
@tweet = Tweet.find(params[:id])
@comment = Comment.new
end
<%= form_with(model:[@tweet, @comment]) do |form| %>
<%= form.text_field :content %>
<%= form.submit %>
<% end %>
複数モデルを受け取ると、1つ目が中身あり、2つ目が中身なしということで、それぞれからtweets/:id/messagesというパスを自動で判断し、さらに中身なしということでmessages_controller.rbのcreateを呼び出します。
def create
Comment.create(comment_params)
end
private
def
# モデルを渡してるから階層構造になっているため.require(:comment)を仲介させる
# .mergeによりtweet_idを紐つける
pramas.requier(:comment).permit[:content].merge(tweet_id: params[:id])
end
察しの良い人はお気づきかもしれませんが、この2つモデルを渡す場合でも、2つ目のモデルに中身が入っていると、Railsが勝手に判断してupdateが行われます!
この部分を図示すると以下のようになります。
これを用いれば、どれだけ深いページ構造になったとしても、同じようにモデルを複数渡してやることによって、簡単に紐付けを行いながらDBに追加していくことが可能になるというわけです!
3. まとめ
とまあこんな感じで伝えたかったことは
・form_withにモデルを渡してやると勝手に適当なメソッド呼び出してくれる。
・form_withにモデルを渡してフォームを作ると、paramsの値が階層構造になる。
→ストロングパラメータの設定には気をつけて!
・form_withに複数モデルを渡すと階層的なルーティングにも対応できるよ
ということでした。
以上で終わりです、開発での参考になってくれれば幸いです。