Railsのform_forとform_withの違いを調べてみた
これは、「フィヨルドブートキャンプ Part 2 Advent Calendar 2023」の19日目の記事です。
昨日の記事は、machidaさんの「[文字起こし] TokyuRuby会議 14 で Tokyu.rb のロゴを作った話の LT をしました🎨」とpenoさんの「Ruby のメソッドのソースコード(C言語)を初めて読んだ」です。
今年のフィヨルドブートキャンプのアドベントカレンダーは、以下となります。
フィヨルドブートキャンプ Part 1 Advent Calendar 2023 - Adventar
フィヨルドブートキャンプ Part 2 Advent Calendar 2023 - Adventar
はじめに
Railsで5.1以降はform_for
、form_tag
の代わりにform_with
を使うことがRailsガイドに言及されています。
しかし、form_for
とform_with
が似たメソッドで違いについてあまり理解できてないです。
類似記事は沢山ありますが、改めて自分で内部実装やドキュメント等を調べて違いを整理しました。
調査した環境のバージョンは以下となります。
- Rails: 7.0.7.2
- actionview:7.0.7
form_withとform_forの違いまとめ
Railsガイドには、form_with
の機能について以下のように記載されてあります。
Rails 5.1でform_withが導入されるまでは、form_withの機能はform_tagとform_forに分かれていました。
つまり、form_with
は、汎用的にform_tag
とform_for
の両機能を利用することが出来るということです。
form_with
とform_for
の違いは結論から言うと、form_with
には以下の2点の特徴があり、form_for
は以下の2点とは逆の特徴を持ちます。
- フォームタグにHTMLの
class
とid
を付与しない - フィールドタグの
method
オプションにモデルの属性以外も指定可能
form_forはform_withを呼び出す
先程特徴の違いを2点書きましたが、form_for
の実装内容を見るとform_with
との違いがより理解しやすいです。
というのも、form_for
のコードを見ると分かりますが処理内容は以下のようになっています。(actionview7.0.7の場合)
form_for
のオプションをform_with
のオプションに加工するform_with
を呼び出す
そのためform_for
とform_with
の違いとなる要素は、form_with
で利用するオプションの違いを比較すれば良いわけです。
実際に例を見てオプションの差を確認します。Messageモデルを指定した場合にform_wiht
内で利用されるオプションを比較してみます。
比較箇所はform_with
で呼び出されるprivateメソッドのhtml_options_for_form_with
で、options
とhtml_options
の値を見ます。
def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms, skip_enforcing_utf8: nil, **options) html_options = options.slice(:id, :class, :multipart, :method, :data, :authenticity_token).merge!(html) html_options[:remote] = html.delete(:remote) || !local html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? if skip_enforcing_utf8.nil? if options.key?(:enforce_utf8) html_options[:enforce_utf8] = options[:enforce_utf8] end else html_options[:enforce_utf8] = !skip_enforcing_utf8 end # ここでの`options`と`html_options`を比較する html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options) end
実行するform_with
とform_for
のコードは下記になります。
比較のためにform_for
とform_with
で指定するオプションは最低限です。
form_for
のコード
form_for @message, url: messages_path do |f| end
form_with
のコード
form_with model: @message, url: messages_path do |f| end
上記2つのコードを実行した場合に、form_with
内で利用されるオプションの違いは以下となります。
options、html_optionsのキー | form_forで呼び出す場合 | form_withで呼び出す場合 |
---|---|---|
options[:allow_method_names_outside_object] | false | true |
options[:skip_default_ids] | false | false |
options[:multipart] | nil | nil |
html_options[:id] | "edit_message_1" | キーなし |
html_options[:class] | "edit_message" | キーなし |
html_options[remote] | false | false |
違いとなるオプションは、:id
、:class
と:allow_method_names_outside_object
です。
:id
と:class
はフォームタグにidを付与するものです。
:allow_method_names_outside_object
はRailsAPIDocsに記載がないですが、フィールドタグにモデル属性以外の許可を設定するものです。
form_forがモデルの属性以外を設定できない理由
form_for
がフィールドタグにモデルの属性以外を指定できない理由を見てみます。
フィールドタグの制約に影響するデータはform_with
オプションの:allow_method_names_outside_object
です。form_for
を呼び出したときの:allow_method_names_outside_object
の値は、先程見たオプションの違いの表からfalse
であることが分かりました。
これを利用するコードはフォームタグのヘルパーメソッドのActionView::Helpers::Tags::Base#value
になります。
フィールドタグのvalue
属性を設定する値を取得するメソッドになります。
def value if @allow_method_names_outside_object object.public_send @method_name if object && object.respond_to?(@method_name) else object.public_send @method_name if object end end
valueメソッドは、
:allow_method_names_outside_object
がtrueの場合は、public_send
を呼び出さないためnilを返します。
一方で:allow_method_names_outside_object
がfalseの場合は、public_send
を呼び出すためメソッドがない場合はエラーが発生します。
このような仕組みによってform_for
がモデル属性以外を指定できないように制御しています。
最後に
今回はRails7の内部実装を見て違いについて理解を深めました。Rails6の場合だと実装内容が異なるので、今回は調査対象外にしました。Rails6からRails7で実装内容を変えた経緯を調べるのも面白そうです。