Rails form_for 源码阅读

初学 Rails 时,觉得 form_for 很强大,这么简单就能完成一个表单。
现在想来,它其实就是通过配置来输出 Html。但由于做法很巧妙,使用时颇为爽快。
于是阅读下源码来研究下它的神奇之处。

如何使用

1
2
3
4
5
6
7
8
9
<%= form_for @person do |f| %>
<%= f.label :first_name %>:
<%= f.text_field :first_name %><br />
<%= f.label :last_name %>:
<%= f.text_field :last_name %><br />
<%= f.submit %>
<% end %>
1
2
3
4
5
6
7
8
9
10
<form action="/people" class="new_person" id="new_person" method="post">
<input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
<label for="person_first_name">First name</label>:
<input id="person_first_name" name="person[first_name]" type="text" /><br />
<label for="person_last_name">Last name</label>:
<input id="person_last_name" name="person[last_name]" type="text" /><br />
<input name="commit" type="submit" value="Create Person" />
</form>

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 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
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
  • form_for 如何获得 @person 的 model name?
    通过 model_name_from_record_or_class(object).param_key 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    module ActionView
    module ModelNaming
    # Converts the given object to an ActiveModel compliant one.
    def convert_to_model(object)
    object.respond_to?(:to_model) ? object.to_model : object
    end
    def model_name_from_record_or_class(record_or_class)
    convert_to_model(record_or_class).model_name
    end
    end
    end
  • 如何判断 @person 是 create 还是 update?
    通过 persisted? 来判断。

    1
    2
    3
    4
    # apply_from_for_options 中的代码
    ...
    action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
    ...
  • FormBuilder 的作用是?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # FormBuilder
    (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

以上是 FormBuilder 中的一段代码,其中 @template 代表的是 FormHelper。可以看出,FormBuilder 其实是一个 FormHelper 方法的代理类。
那为什么不直接使用 FormHelper?我的理解是,通过加入一层抽象,增加了灵活性。比如可以通过扩展 FormBuilder 来实现自定义的表单功能。

  • 最终的 html 是怎么生成的?
    capture(builder, &block) 中通过 FormBuilder 的方法来生成第一步的html,再通过 form_tag_with_body(html_options, output) 替换掉生成 html 中的 form_tag,形成最终版。
    1
    2
    3
    4
    5
    6
    7
    def capture(*args)
    value = nil
    buffer = with_output_buffer { value = yield(*args) }
    if string = buffer.presence || value and string.is_a?(String)
    ERB::Util.html_escape string
    end
    end