样本

跟踪一个简单的form_for:

<%= binding.trace_tree(html: true, tmp: ['dapo', 'text_field.html'], timer: true){ form_for :article do |f| %>
  

    <%= f.text_field :title %>  

<% end } %>


得如下调用栈(完整html在结尾)



form_for主要干的是:new一个form_builder_class,然后传给block,使能够在block中在调用各种tag方法生成String。通常此过程会写在erb中,使erb将各个tag返回的String拼接起来,嵌进一个form_for生成的String中

form_for调text_field

下面两句展示了“new一个form_builder_class,然后传给block ”这个步骤

builder = instantiate_builder(object_name, object, options)
output  = capture(builder, &block)


这个builder目前看来是ActionView::Helpers::FormBuilder,应该是走了default_form_builder,而default_form_builder定义如下

ActiveSupport.on_load(:action_view) do
  cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do
    ::ActionView::Helpers::FormBuilder
  end
end


text_field的定义如下

(field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector|
  class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
    def #{selector}(method, options = {})  # def text_field(method, options = {})
      @template.send(                      #   @template.send(
        #{selector.inspect},               #     "text_field",
        @object_name,                      #     @object_name,
        method,                            #     method,
        objectify_options(options))        #     objectify_options(options))
    end                                    # end
  RUBY_EVAL
end


@template正是ActionView::Helpers::FormBuilder本身self(至于为什么不直接调用,未明……)

builder.new(object_name, object, self, options)


ActionView::Helpers::FormBuilder本身的text_field方法如下,会传object_name和method

def text_field(object_name, method, options = {})
  Tags::TextField.new(object_name, method, self, options).render
end


object_name来自于form_for第一个参数

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

#...

def instantiate_builder(record_name, record_object, options)
  case record_name
  when String, Symbol
    object = record_object
    object_name = record_name
  else
    object = record_name
    object_name = model_name_from_record_or_class(object).param_key
  end

#...


method来自于text_field第一个参数

text_field的field name

text_field调用栈如下


TextField继承自Base,只重写了Base未实现的render

module ActionView
  module Helpers
    module Tags # :nodoc:
      class TextField < Base # :nodoc:
        include Placeholderable

        def render
          options = @options.stringify_keys
          options["size"] = options["maxlength"] unless options.key?("size")
          options["type"] ||= field_type
          options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file"
          add_default_name_and_id(options)
          tag("input", options)
        end
#...


而name的拼接在Base的add_default_name_and_id中实现,就是object_name后加中括号包围method,额外或许会有index和数组形式的[]

def add_default_name_and_id(options)
  index = name_and_id_index(options)
  options["name"] = options.fetch("name"){ tag_name(options["multiple"], index) }
  options["id"] = options.fetch("id"){ tag_id(index) }
  if namespace = options.delete("namespace")
    options['id'] = options['id'] ? "#{namespace}_#{options['id']}" : namespace
  end
end

def tag_name(multiple = false, index = nil)
  # a little duplication to construct less strings
  if index
    "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
  else
    "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
  end
end


完整trace html

20170218_214926_532_text_field.html