对于最基本的CRUD,new和edit中的form一般都被抽成一个partial,然后render 'form'来避免重复,因为对于最基本的新增、修改,这个form基本上是一样的,如下

<%= form_for @student do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  
  <%= f.label :grade %>   <%= f.text_field :grade %>  
  <%= f.submit %> <% end %>


那么rails在接收这个form时怎样判断该调controller的create还是update呢?其实这在生成form的时候就决定了

根据rails的约定(或REST的约定),新增一个student,就该向/students发POST,修改一个student,就该向/students/:id发PATCH。因此,form标签的action应该一开始就根据你要做的动作而带上不同的url(确实form_for自动生成的action是有不同),那么它是怎么做到的呢

于是,在打开/students/new和/students/1/edit时,跟踪一下

<%= binding.trace_tree(htmp: 'rails/form_for') { form_for @student do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  
  <%= f.label :grade %>   <%= f.text_field :grade %>  
  <%= f.submit %> <% end }%>


edit调用栈如下

20170708_230054_609_form_for.html

new调用栈如下

20170708_231341_059_form_for.html

入口如下,可以看到,form_for中的options[:url]如果不提供,就会从polymorphic_path(record获取

# actionview-5.0.4/lib/action_view/helpers/form_helper.rb
def form_for(record, options = {}, &block)
  raise ArgumentError, "Missing block" unless block_given?
  html_options = options[:html] ||= {}
  case record
  when String, Symbol
    object_name = record
    object      = nil
  else
    object      = record.is_a?(Array) ? record.last : record
    raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
    object_name = options[:as] || model_name_from_record_or_class(object).param_key
    apply_form_for_options!(record, object, options)
  end
  html_options[:data]   = options.delete(:data)   if options.has_key?(:data)
  html_options[:remote] = options.delete(:remote) if options.has_key?(:remote)
  html_options[:method] = options.delete(:method) if options.has_key?(:method)
  html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8)
  html_options[:authenticity_token] = options.delete(:authenticity_token)

  builder = instantiate_builder(object_name, object, options)
  output  = capture(builder, &block)
  html_options[:multipart] ||= builder.multipart?

  html_options = html_options_for_form(options[:url] || {}, html_options)
  form_tag_with_body(html_options, output)
end

def apply_form_for_options!(record, object, options) #:nodoc:
  object = convert_to_model(object)

  as = options[:as]
  namespace = options[:namespace]
  action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
  options[:html].reverse_merge!(
    class:  as ? "#{action}_#{as}" : dom_class(object, action),
    id:     (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
    method: method
  )

  options[:url] ||= if options.key?(:format)
                      polymorphic_path(record, format: options.delete(:format))
                    else
                      polymorphic_path(record, {})
                    end
end


而polymorphic_path经过某些判断,得出要调student_path还是students_path来生成url

edit如下


new如下


所谓某些判断,其实就是persisted?,从源码可见当persisted?为true时,要用model.model_name.singular_route_key的方法,也就是student,否则,students

# actionpack-5.0.4/lib/action_dispatch/routing/polymorphic_routes.rb
def handle_model(record)
  args  = []

  model = record.to_model
  named_route = if model.persisted?
                  args << model
                  get_method_for_string model.model_name.singular_route_key
                else
                  get_method_for_class model
                end

  [named_route, args]
end


同理,form_for中的submit,若果没有给名字(第一个参数),也是会根据model是否persisted?来生成名为update或create的提交按钮

其逻辑如下


也就是源码中value ||= submit_default_value这句

# actionview-5.0.4/lib/action_view/helpers/form_helper.rb
def submit(value=nil, options={})
  value, options = nil, value if value.is_a?(Hash)
  value ||= submit_default_value
  @template.submit_tag(value, options)
end


submit_default_value如下

# actionview-5.0.4/lib/action_view/helpers/form_helper.rb
def submit_default_value
  object = convert_to_model(@object)
  key    = object ? (object.persisted? ? :update : :create) : :submit

  model = if object.respond_to?(:model_name)
    object.model_name.human
  else
    @object_name.to_s.humanize
  end

  defaults = []
  defaults << :"helpers.submit.#{object_name}.#{key}"
  defaults << :"helpers.submit.#{key}"
  defaults << "#{key.to_s.humanize} #{model}"

  I18n.t(defaults.shift, model: model, default: defaults)
end


总的来说,根据form_for对象是否persisted?,生成的form也会不同

edit已persisted?的,就这样


new的,就这样