概要
Rails の form_with ヘルパーでは、method オプション(任意オプション)を指定すると form_with によって生成される html の form タグに対して HTTP メソッドを任意に決めることができる。
未指定の場合はデフォルトで POST
になるのかと思っていた(ここに書いてあったのでそう思っていた)が、そうでもなさそうだったので未指定の場合の挙動について今回調べてみた。
追記
たしかに form タグ自体の method のデフォルトは POST
になる。
記事執筆時の環境
- Ruby on Rails: 7.0.4
結論
例えば、以下のように form_with
を指定したとき( method
オプションが未指定のとき)、@model
が保存済み ( persisted?
) かどうかで HTTP メソッドが決まる。
/ erb ではなく slim で記述している = form_with model: @model, local: true do |f| ~省略~
保存済みの場合:
POST
<form action="/models/:id" accept-charset="UTF-8" method="post"> <input type="hidden" name="authenticity_token" value="~省略~" autocomplete="off">
未保存の場合:
PATCH
name="_method"
の input タグが追加される。<form action="/models/:id" accept-charset="UTF-8" method="post"> <input type="hidden" name="_method" value="patch" autocomplete="off"> <input type="hidden" name="authenticity_token" value="~省略~" autocomplete="off">
詳しく見てみる
結論にも書いたが、以下のような記述で form_with
を使ってみる。
(新規作成時 / 編集時 の両方の画面で使えるようなかたちにしてみた)
/ erb ではなく slim で記述している = form_with model: @model, local: true do |f| ~省略~
ここからコードを探っていく。
まず、form_with
ヘルパーは以下で定義されている。
def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block) options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options) if model if url != false url ||= polymorphic_path(model, format: format) end model = _object_for_form_builder(model) scope ||= model_name_from_record_or_class(model).param_key end if block_given? builder = instantiate_builder(scope, model, options) output = capture(builder, &block) options[:multipart] ||= builder.multipart? html_options = html_options_for_form_with(url, model, **options) form_tag_with_body(html_options, output) else html_options = html_options_for_form_with(url, model, **options) form_tag_html(html_options) end end
今回のサンプルコードの場合だと、ブロックを渡しているので if block_given?
の条件分岐は true になる。
(ただ、今回の調査内容でいうと block_given?
かどうかは調査結果に関係ない)
どちらにせよ、form_tag_with_body
もしくは form_tag_html
で html の form タグが作られそうである。
ひとまず、その前の html_options_for_form_with
の中身を見てみると、html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
という記述があり、ここでとりあえず model が保存済みなら PATCH
になることがわかる。
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 html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options) end
form_with
メソッドに戻って、form_tag_with_body と form_tag_html を見ると、form_tag_with_body
は form_tag_html
を呼び出している。
どちらも途中で extra_tags_for_form
というメソッドを呼んでおり、ここでいかにも method を決定していそうなことがわかった。
def extra_tags_for_form(html_options) authenticity_token = html_options.delete("authenticity_token") method = html_options.delete("method").to_s.downcase method_tag = \ case method when "get" html_options["method"] = "get" "" when "post", "" html_options["method"] = "post" token_tag(authenticity_token, form_options: { action: html_options["action"], method: "post" }) else html_options["method"] = "post" method_tag(method) + token_tag(authenticity_token, form_options: { action: html_options["action"], method: method }) end if html_options.delete("enforce_utf8") { default_enforce_utf8 } utf8_enforcer_tag + method_tag else method_tag end end
今回は model オプションに指定した @model
が保存済みかどうかで html_options_for_form_with
のところで html_options[:method]
に :patch
か nil が入るようになっていた。
なので、method_tag
に代入している switch 文では else
か when "post", ""
内の処理が実行されることになる。
( html_options[:method]
が nil の場合は extra_tags_for_form
で method = html_options.delete("method").to_s.downcase
により ""
になるので when "post", ""
内に入る)
まとめると、
まずは
@model
が保存済みかどうか関係なく、html_options["method"] = "post"
が設定される。これは
<form action="/models/:id" accept-charset="UTF-8" method="post">
の method の部分。@model
が保存済みなら、method_tag(method)
( method は:patch
) が実行され、<input type="hidden" name="_method" value="patch" autocomplete="off">
の部分が作られる。