跟踪一个简单的form_for
样本
跟踪一个简单的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