form_helper.rb 34.2 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1
require 'cgi'
2 3
require 'action_view/helpers/date_helper'
require 'action_view/helpers/tag_helper'
4
require 'action_view/helpers/form_tag_helper'
D
Initial  
David Heinemeier Hansson 已提交
5 6 7

module ActionView
  module Helpers
8 9 10 11
    # Form helpers are designed to make working with models much easier compared to using just standard HTML
    # elements by providing a set of methods for creating forms based on your models. This helper generates the HTML
    # for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form
    # is submitted (i.e., when the user hits the submit button or <tt>form.submit</tt> is called via JavaScript), the form inputs will be bundled into the <tt>params</tt> object and passed back to the controller.
D
David Heinemeier Hansson 已提交
12
    #
13
    # There are two types of form helpers: those that specifically work with model attributes and those that don't.
14 15
    # This helper deals with those that work with model attributes; to see an example of form helpers that don't work
    # with model attributes, check the ActionView::Helpers::FormTagHelper documentation.
D
David Heinemeier Hansson 已提交
16
    #
17
    # The core method of this helper, form_for, gives you the ability to create a form for a model instance;
18
    # for example, let's say that you have a model <tt>Person</tt> and want to create a new instance of it:
D
Initial  
David Heinemeier Hansson 已提交
19
    #
20
    #     # Note: a @person variable will have been created in the controller.
21 22
    #     # For example: @person = Person.new
    #     <% form_for :person, @person, :url => { :action => "create" } do |f| %>
D
David Heinemeier Hansson 已提交
23 24
    #       <%= f.text_field :first_name %>
    #       <%= f.text_field :last_name %>
25
    #       <%= submit_tag 'Create' %>
D
David Heinemeier Hansson 已提交
26
    #     <% end %>
D
Initial  
David Heinemeier Hansson 已提交
27
    #
28
    # The HTML generated for this would be:
D
Initial  
David Heinemeier Hansson 已提交
29
    #
30
    #     <form action="/persons/create" method="post">
D
David Heinemeier Hansson 已提交
31 32
    #       <input id="person_first_name" name="person[first_name]" size="30" type="text" />
    #       <input id="person_last_name" name="person[last_name]" size="30" type="text" />
33
    #       <input name="commit" type="submit" value="Create" />
D
David Heinemeier Hansson 已提交
34
    #     </form>
D
Initial  
David Heinemeier Hansson 已提交
35
    #
36 37 38 39 40 41 42 43 44
    # If you are using a partial for your form fields, you can use this shortcut:
    #
    #     <% form_for :person, @person, :url => { :action => "create" } do |f| %>
    #       <%= render :partial => f %>
    #       <%= submit_tag 'Create' %>
    #     <% end %>
    #
    # This example will render the <tt>people/_form</tt> partial, setting a local variable called <tt>form</tt> which references the yielded FormBuilder.
    #
45
    # The <tt>params</tt> object created when this form is submitted would look like:
D
Initial  
David Heinemeier Hansson 已提交
46
    #
47
    #     {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}}
48
    #
49 50 51
    # The params hash has a nested <tt>person</tt> value, which can therefore be accessed with <tt>params[:person]</tt> in the controller.
    # If were editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than <tt>Person.new</tt> in the controller), the objects
    # attribute values are filled into the form (e.g., the <tt>person_first_name</tt> field would have that person's first name in it).
52
    #
53
    # If the object name contains square brackets the id for the object will be inserted. For example:
54
    #
55
    #   <%= text_field "person[]", "name" %>
56
    #
57
    # ...will generate the following ERb.
58 59 60
    #
    #   <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" />
    #
61
    # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial
62
    # used by <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may come in handy. Example:
63 64 65
    #
    #   <%= text_field "person", "name", "index" => 1 %>
    #
66
    # ...becomes...
67
    #
68 69
    #   <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" />
    #
70 71 72
    # An <tt>index</tt> option may also be passed to <tt>form_for</tt> and <tt>fields_for</tt>.  This automatically applies
    # the <tt>index</tt> to all the nested fields.
    #
D
David Heinemeier Hansson 已提交
73
    # There are also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
D
Initial  
David Heinemeier Hansson 已提交
74 75
    # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
    module FormHelper
76 77
      # Creates a form and a scope around a specific model object that is used as
      # a base for questioning about values for the fields.
78
      #
79 80 81 82 83 84 85 86
      # Rails provides succint resource-oriented form generation with +form_for+
      # like this:
      #
      #   <% form_for @offer do |f| %>
      #     <%= f.label :version, 'Version' %>:
      #     <%= f.text_field :version %><br />
      #     <%= f.label :author, 'Author' %>:
      #     <%= f.text_field :author %><br />
87 88
      #   <% end %>
      #
89 90 91
      # There, +form_for+ is able to generate the rest of RESTful parameters
      # based on introspection on the record, but to understand what it does we
      # need to dig first into the alternative generic usage it is based upon.
92
      #
93 94 95 96 97 98 99 100 101 102 103
      # === Generic form_for
      #
      # The generic way to call +form_for+ requires a few arguments:
      #
      #   <% form_for :person, @person, :url => { :action => "update" } do |f| %>
      #     <%= f.error_messages %>
      #     First name: <%= f.text_field :first_name %><br />
      #     Last name : <%= f.text_field :last_name %><br />
      #     Biography : <%= f.text_area :biography %><br />
      #     Admin?    : <%= f.check_box :admin %><br />
      #   <% end %>
104
      #
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
      # Worth noting is that the +form_for+ tag is called in a ERb evaluation block,
      # not an ERb output block. So that's <tt><% %></tt>, not <tt><%= %></tt>. Also
      # worth noting is that +form_for+ yields a form builder object, in this
      # example as +f+, which emulates the API for the stand-alone FormHelper
      # methods, but without the object name. So instead of <tt>text_field :person, :name</tt>,
      # you get away with <tt>f.text_field :name</tt>. Notice that you can even do
      # <tt><%= f.error_messages %></tt> to display the error messsages of the model
      # object in question.
      #
      # Even further, the +form_for+ method allows you to more easily escape the
      # instance variable convention. So while the stand-alone approach would require
      # <tt>text_field :person, :name, :object => person</tt> to work with local
      # variables instead of instance ones, the +form_for+ calls remain the same.
      # You simply declare once with <tt>:person, person</tt> and all subsequent
      # field calls save <tt>:person</tt> and <tt>:object => person</tt>.
      #
      # Also note that +form_for+ doesn't create an exclusive scope. It's still
      # possible to use both the stand-alone FormHelper methods and methods from
      # FormTagHelper. For example:
124
      #
125
      #   <% form_for :person, @person, :url => { :action => "update" } do |f| %>
126 127 128 129 130 131
      #     First name: <%= f.text_field :first_name %>
      #     Last name : <%= f.text_field :last_name %>
      #     Biography : <%= text_area :person, :biography %>
      #     Admin?    : <%= check_box_tag "person[admin]", @person.company.admin? %>
      #   <% end %>
      #
132 133 134
      # This also works for the methods in FormOptionHelper and DateHelper that are
      # designed to work with an object as base, like FormOptionHelper#collection_select
      # and DateHelper#datetime_select.
135
      #
136 137
      # HTML attributes for the form tag can be given as <tt>:html => {...}</tt>.
      # For example:
138
      #
139 140 141 142
      #   <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %>
      #     ...
      #   <% end %>
      #
143 144
      # The above form will then have the +id+ attribute with the value "person_form",
      # which you can then style with CSS or manipulate with JavaScript.
145
      #
146 147
      # === Relying on record identification
      #
148 149 150 151
      # As we said above, in addition to manually configuring the +form_for+ call,
      # you can rely on record identification, which will use the conventions and
      # named routes of that approach. This is the preferred way to use +form_for+
      # nowadays:
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
      #
      #   <% form_for(@post) do |f| %>
      #     ...
      #   <% end %>
      #
      # This will expand to be the same as:
      #
      #   <% form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
      #     ...
      #   <% end %>
      #
      # And for new records:
      #
      #   <% form_for(Post.new) do |f| %>
      #     ...
      #   <% end %>
      #
      # This will expand to be the same as:
      #
171
      #   <% form_for :post, Post.new, :url => posts_path, :html => { :class => "new_post", :id => "new_post" } do |f| %>
172 173 174 175 176 177 178 179 180
      #     ...
      #   <% end %>
      #
      # You can also overwrite the individual conventions, like this:
      #
      #   <% form_for(@post, :url => super_post_path(@post)) do |f| %>
      #     ...
      #   <% end %>
      #
181
      # And for namespaced routes, like +admin_post_url+:
182 183 184 185 186
      #
      #   <% form_for([:admin, @post]) do |f| %>
      #    ...
      #   <% end %>
      #
187 188
      # === Customized form builders
      #
189
      # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
190 191
      # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs.
      #
192 193 194 195 196 197
      #   <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %>
      #     <%= f.text_field :first_name %>
      #     <%= f.text_field :last_name %>
      #     <%= text_area :person, :biography %>
      #     <%= check_box_tag "person[admin]", @person.company.admin? %>
      #   <% end %>
198
      #
199 200 201 202 203 204
      # In this case, if you use this:
      #
      #   <%= render :partial => f %>
      #
      # The rendered template is <tt>people/_labelling_form</tt> and the local variable referencing the form builder is called <tt>labelling_form</tt>.
      #
205
      # In many cases you will want to wrap the above in another helper, so you could do something like the following:
206
      #
207 208
      #   def labelled_form_for(record_or_name_or_array, *args, &proc)
      #     options = args.extract_options!
209
      #     form_for(record_or_name_or_array, *(args << options.merge(:builder => LabellingFormBuilder)), &proc)
210 211
      #   end
      #
212
      # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag.
213
      def form_for(record_or_name_or_array, *args, &proc)
214
        raise ArgumentError, "Missing block" unless block_given?
215

216
        options = args.extract_options!
217

218
        case record_or_name_or_array
219
        when String, Symbol
220
          object_name = record_or_name_or_array
221
        when Array
222
          object = record_or_name_or_array.last
223
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
224
          apply_form_for_options!(record_or_name_or_array, options)
225
          args.unshift object
226
        else
227
          object = record_or_name_or_array
228
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
229
          apply_form_for_options!([object], options)
230
          args.unshift object
231 232
        end

233
        concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding)
234
        fields_for(object_name, *(args << options), &proc)
235
        concat('</form>', proc.binding)
236
      end
237

238 239
      def apply_form_for_options!(object_or_array, options) #:nodoc:
        object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array
240

241 242 243 244 245 246 247
        html_options =
          if object.respond_to?(:new_record?) && object.new_record?
            { :class  => dom_class(object, :new),  :id => dom_id(object), :method => :post }
          else
            { :class  => dom_class(object, :edit), :id => dom_id(object, :edit), :method => :put }
          end

248 249
        options[:html] ||= {}
        options[:html].reverse_merge!(html_options)
250
        options[:url] ||= polymorphic_path(object_or_array)
251
      end
252 253

      # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes
254
      # fields_for suitable for specifying additional model objects in the same form:
255
      #
256
      # ==== Examples
257
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
258 259
      #     First name: <%= person_form.text_field :first_name %>
      #     Last name : <%= person_form.text_field :last_name %>
260
      #
261
      #     <% fields_for @person.permission do |permission_fields| %>
262 263 264 265
      #       Admin?  : <%= permission_fields.check_box :admin %>
      #     <% end %>
      #   <% end %>
      #
266 267 268 269 270 271 272 273 274 275 276 277
      # ...or if you have an object that needs to be represented as a different parameter, like a Client that acts as a Person:
      #
      #   <% fields_for :person, @client do |permission_fields| %>
      #     Admin?: <%= permission_fields.check_box :admin %>
      #   <% end %>
      #
      # ...or if you don't have an object, just a name of the parameter
      #
      #   <% fields_for :person do |permission_fields| %>
      #     Admin?: <%= permission_fields.check_box :admin %>
      #   <% end %>
      #
278 279
      # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base,
      # like FormOptionHelper#collection_select and DateHelper#datetime_select.
280
      def fields_for(record_or_name_or_array, *args, &block)
281
        raise ArgumentError, "Missing block" unless block_given?
282
        options = args.extract_options!
283 284 285 286 287 288 289 290 291 292 293 294 295

        case record_or_name_or_array
        when String, Symbol
          object_name = record_or_name_or_array
          object = args.first
        when Array
          object = record_or_name_or_array.last
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
          apply_form_for_options!(record_or_name_or_array, options)
        else
          object = record_or_name_or_array
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
        end
296 297 298

        builder = options[:builder] || ActionView::Base.default_form_builder
        yield builder.new(object_name, object, self, options, block)
299 300
      end

301 302 303
      # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
      # assigned to the template (identified by +object+). The text of label will default to the attribute name unless you specify
      # it explicitly. Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
304
      # onto the HTML as an HTML element attribute as in the example shown.
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
      #
      # ==== Examples
      #   label(:post, :title)
      #   #=> <label for="post_title">Title</label>
      #
      #   label(:post, :title, "A short title")
      #   #=> <label for="post_title">A short title</label>
      #
      #   label(:post, :title, "A short title", :class => "title_label")
      #   #=> <label for="post_title" class="title_label">A short title</label>
      #
      def label(object_name, method, text = nil, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_label_tag(text, options)
      end

D
Initial  
David Heinemeier Hansson 已提交
320 321
      # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
      # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
322
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
D
David Heinemeier Hansson 已提交
323
      # shown.
D
Initial  
David Heinemeier Hansson 已提交
324
      #
325
      # ==== Examples
D
David Heinemeier Hansson 已提交
326
      #   text_field(:post, :title, :size => 20)
327 328 329 330 331 332 333 334 335 336 337
      #   # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
      #
      #   text_field(:post, :title, :class => "create_input")
      #   # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
      #
      #   text_field(:session, :user, :onchange => "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }")
      #   # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange = "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }"/>
      #
      #   text_field(:snippet, :code, :size => 20, :class => 'code_input')
      #   # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
      #
338 339
      def text_field(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options)
D
Initial  
David Heinemeier Hansson 已提交
340 341
      end

342 343
      # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
      # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
344
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
      # shown.
      #
      # ==== Examples
      #   password_field(:login, :pass, :size => 20)
      #   # => <input type="text" id="login_pass" name="login[pass]" size="20" value="#{@login.pass}" />
      #
      #   password_field(:account, :secret, :class => "form_input")
      #   # => <input type="text" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
      #
      #   password_field(:user, :password, :onchange => "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }")
      #   # => <input type="text" id="user_password" name="user[password]" value="#{@user.password}" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/>
      #
      #   password_field(:account, :pin, :size => 20, :class => 'form_input')
      #   # => <input type="text" id="account_pin" name="account[pin]" size="20" value="#{@account.pin}" class="form_input" />
      #
360 361
      def password_field(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options)
D
Initial  
David Heinemeier Hansson 已提交
362 363
      end

364 365
      # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
      # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
366
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
367 368
      # shown.
      #
369
      # ==== Examples
370 371 372 373 374 375 376
      #   hidden_field(:signup, :pass_confirm)
      #   # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
      #
      #   hidden_field(:post, :tag_list)
      #   # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
      #
      #   hidden_field(:user, :token)
377
      #   # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
378
      def hidden_field(object_name, method, options = {})
379
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options)
D
Initial  
David Heinemeier Hansson 已提交
380 381
      end

382 383
      # Returns an file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
      # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
384
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
385 386 387 388 389 390 391 392 393 394 395 396
      # shown.
      #
      # ==== Examples
      #   file_field(:user, :avatar)
      #   # => <input type="file" id="user_avatar" name="user[avatar]" />
      #
      #   file_field(:post, :attached, :accept => 'text/html')
      #   # => <input type="file" id="post_attached" name="post[attached]" />
      #
      #   file_field(:attachment, :file, :class => 'file_input')
      #   # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
      #
397 398
      def file_field(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options)
399 400
      end

D
Initial  
David Heinemeier Hansson 已提交
401 402 403 404
      # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
      # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
      # hash with +options+.
      #
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
      # ==== Examples
      #   text_area(:post, :body, :cols => 20, :rows => 40)
      #   # => <textarea cols="20" rows="40" id="post_body" name="post[body]">
      #   #      #{@post.body}
      #   #    </textarea>
      #
      #   text_area(:comment, :text, :size => "20x30")
      #   # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
      #   #      #{@comment.text}
      #   #    </textarea>
      #
      #   text_area(:application, :notes, :cols => 40, :rows => 15, :class => 'app_input')
      #   # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input">
      #   #      #{@application.notes}
      #   #    </textarea>
      #
      #   text_area(:entry, :body, :size => "20x20", :disabled => 'disabled')
      #   # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled">
      #   #      #{@entry.body}
      #   #    </textarea>
425 426
      def text_area(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options)
D
Initial  
David Heinemeier Hansson 已提交
427
      end
428

D
Initial  
David Heinemeier Hansson 已提交
429 430 431 432
      # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
      # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that
      # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a
      # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+
433 434
      # is set to 0 which is convenient for boolean values. Since HTTP standards say that unchecked checkboxes don't post anything,
      # we add a hidden value with the same name as the checkbox as a work around.
D
Initial  
David Heinemeier Hansson 已提交
435
      #
436
      # ==== Examples
437
      #   # Let's say that @post.validated? is 1:
D
Initial  
David Heinemeier Hansson 已提交
438
      #   check_box("post", "validated")
P
Pratik Naik 已提交
439
      #   # => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
440
      #   #    <input name="post[validated]" type="hidden" value="0" />
D
Initial  
David Heinemeier Hansson 已提交
441
      #
442
      #   # Let's say that @puppy.gooddog is "no":
D
Initial  
David Heinemeier Hansson 已提交
443
      #   check_box("puppy", "gooddog", {}, "yes", "no")
444 445 446
      #   # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
      #   #    <input name="puppy[gooddog]" type="hidden" value="no" />
      #
P
Pratik Naik 已提交
447 448
      #   check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no")
      #   # => <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
449 450
      #   #    <input name="eula[accepted]" type="hidden" value="no" />
      #
451 452
      def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
453
      end
454 455 456 457

      # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
      # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
      # radio button will be checked. Additional options on the input tag can be passed as a
458
      # hash with +options+.
459 460 461
      #
      # ==== Examples
      #   # Let's say that @post.category returns "rails":
462 463
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
P
Pratik Naik 已提交
464 465
      #   # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
      #   #    <input type="radio" id="post_category_java" name="post[category]" value="java" />
466
      #
467 468
      #   radio_button("user", "receive_newsletter", "yes")
      #   radio_button("user", "receive_newsletter", "no")
P
Pratik Naik 已提交
469 470
      #   # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
      #   #    <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
471 472
      def radio_button(object_name, method, tag_value, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options)
473
      end
D
Initial  
David Heinemeier Hansson 已提交
474 475 476
    end

    class InstanceTag #:nodoc:
477
      include Helpers::TagHelper, Helpers::FormTagHelper
D
Initial  
David Heinemeier Hansson 已提交
478 479

      attr_reader :method_name, :object_name
480 481

      DEFAULT_FIELD_OPTIONS     = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS)
482
      DEFAULT_RADIO_OPTIONS     = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS)
483
      DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS)
D
Initial  
David Heinemeier Hansson 已提交
484

485
      def initialize(object_name, method_name, template_object, local_binding = nil, object = nil)
486
        @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
D
Initial  
David Heinemeier Hansson 已提交
487
        @template_object, @local_binding = template_object, local_binding
488
        @object = object
489
        if @object_name.sub!(/\[\]$/,"")
490 491
          if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
            @auto_index = object.to_param
492
          else
493
            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
494
          end
495
        end
D
Initial  
David Heinemeier Hansson 已提交
496
      end
497

498
      def to_label_tag(text = nil, options = {})
499
        options = options.stringify_keys
500 501
        name_and_id = options.dup
        add_default_name_and_id(name_and_id)
502
        options.delete("index")
503
        options["for"] ||= name_and_id["id"]
504
        content = (text.blank? ? nil : text.to_s) || method_name.humanize
505
        label_tag(name_and_id["id"], content, options)
506 507
      end

D
Initial  
David Heinemeier Hansson 已提交
508
      def to_input_field_tag(field_type, options = {})
509
        options = options.stringify_keys
510
        options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
511
        options = DEFAULT_FIELD_OPTIONS.merge(options)
512 513 514 515
        if field_type == "hidden"
          options.delete("size")
        end
        options["type"] = field_type
516
        options["value"] ||= value_before_type_cast(object) unless field_type == "file"
517
        options["value"] &&= html_escape(options["value"])
518 519 520 521 522
        add_default_name_and_id(options)
        tag("input", options)
      end

      def to_radio_button_tag(tag_value, options = {})
523
        options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
524 525
        options["type"]     = "radio"
        options["value"]    = tag_value
526 527 528 529 530 531
        if options.has_key?("checked")
          cv = options.delete "checked"
          checked = cv == true || cv == "checked"
        else
          checked = self.class.radio_button_checked?(value(object), tag_value)
        end
532
        options["checked"]  = "checked" if checked
533
        pretty_tag_value    = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
534
        options["id"]     ||= defined?(@auto_index) ?
535 536
          "#{tag_id_with_index(@auto_index)}_#{pretty_tag_value}" :
          "#{tag_id}_#{pretty_tag_value}"
537 538 539 540
        add_default_name_and_id(options)
        tag("input", options)
      end

D
Initial  
David Heinemeier Hansson 已提交
541
      def to_text_area_tag(options = {})
542
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
D
Initial  
David Heinemeier Hansson 已提交
543
        add_default_name_and_id(options)
544 545

        if size = options.delete("size")
546
          options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
547 548
        end

549
        content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
D
Initial  
David Heinemeier Hansson 已提交
550 551 552
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
553 554 555
        options = options.stringify_keys
        options["type"]     = "checkbox"
        options["value"]    = checked_value
556 557 558
        if options.has_key?("checked")
          cv = options.delete "checked"
          checked = cv == true || cv == "checked"
559
        else
560
          checked = self.class.check_box_checked?(value(object), checked_value)
561
        end
562
        options["checked"] = "checked" if checked
D
Initial  
David Heinemeier Hansson 已提交
563
        add_default_name_and_id(options)
564
        tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
565 566 567
      end

      def to_boolean_select_tag(options = {})
568
        options = options.stringify_keys
D
Initial  
David Heinemeier Hansson 已提交
569
        add_default_name_and_id(options)
570
        value = value(object)
D
Initial  
David Heinemeier Hansson 已提交
571 572 573 574 575 576 577 578
        tag_text = "<select"
        tag_text << tag_options(options)
        tag_text << "><option value=\"false\""
        tag_text << " selected" if value == false
        tag_text << ">False</option><option value=\"true\""
        tag_text << " selected" if value
        tag_text << ">True</option></select>"
      end
579

580
      def to_content_tag(tag_name, options = {})
581
        content_tag(tag_name, value(object), options)
582
      end
583

D
Initial  
David Heinemeier Hansson 已提交
584
      def object
585
        @object || (@template_object.instance_variable_get("@#{@object_name}") rescue nil)
D
Initial  
David Heinemeier Hansson 已提交
586 587
      end

588 589
      def value(object)
        self.class.value(object, @method_name)
D
Initial  
David Heinemeier Hansson 已提交
590 591
      end

592 593 594
      def value_before_type_cast(object)
        self.class.value_before_type_cast(object, @method_name)
      end
595

596 597 598 599
      class << self
        def value(object, method_name)
          object.send method_name unless object.nil?
        end
600

601 602 603 604 605 606 607
        def value_before_type_cast(object, method_name)
          unless object.nil?
            object.respond_to?(method_name + "_before_type_cast") ?
            object.send(method_name + "_before_type_cast") :
            object.send(method_name)
          end
        end
608

609 610 611 612 613 614 615 616 617 618 619 620 621 622
        def check_box_checked?(value, checked_value)
          case value
          when TrueClass, FalseClass
            value
          when NilClass
            false
          when Integer
            value != 0
          when String
            value == checked_value
          else
            value.to_i != 0
          end
        end
623

624 625
        def radio_button_checked?(value, checked_value)
          value.to_s == checked_value.to_s
626
        end
627 628
      end

D
Initial  
David Heinemeier Hansson 已提交
629 630
      private
        def add_default_name_and_id(options)
631 632 633
          if options.has_key?("index")
            options["name"] ||= tag_name_with_index(options["index"])
            options["id"]   ||= tag_id_with_index(options["index"])
634
            options.delete("index")
635
          elsif defined?(@auto_index)
636 637
            options["name"] ||= tag_name_with_index(@auto_index)
            options["id"]   ||= tag_id_with_index(@auto_index)
638
          else
639
            options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
640
            options["id"]   ||= tag_id
641
          end
D
Initial  
David Heinemeier Hansson 已提交
642
        end
643

D
Initial  
David Heinemeier Hansson 已提交
644
        def tag_name
645
          "#{@object_name}[#{sanitized_method_name}]"
D
Initial  
David Heinemeier Hansson 已提交
646
        end
647

648
        def tag_name_with_index(index)
649
          "#{@object_name}[#{index}][#{sanitized_method_name}]"
650
        end
D
Initial  
David Heinemeier Hansson 已提交
651 652

        def tag_id
653
          "#{sanitized_object_name}_#{sanitized_method_name}"
D
Initial  
David Heinemeier Hansson 已提交
654
        end
655 656

        def tag_id_with_index(index)
657
          "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
658 659 660
        end

        def sanitized_object_name
661 662 663 664 665
          @sanitized_object_name ||= @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
        end

        def sanitized_method_name
          @sanitized_method_name ||= @method_name.sub(/\?$/,"")
666
        end
D
Initial  
David Heinemeier Hansson 已提交
667
    end
668

669
    class FormBuilder #:nodoc:
670 671 672
      # The methods which wrap a form helper call.
      class_inheritable_accessor :field_helpers
      self.field_helpers = (FormHelper.instance_methods - ['form_for'])
673

674
      attr_accessor :object_name, :object, :options
675

676
      def initialize(object_name, object, template, options, proc)
677
        @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
678
        @default_options = @options ? @options.slice(:index) : {}
679
      end
680

681
      (field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
682 683
        src = <<-end_src
          def #{selector}(method, options = {})
684
            @template.send(#{selector.inspect}, @object_name, method, objectify_options(options))
685 686 687 688
          end
        end_src
        class_eval src, __FILE__, __LINE__
      end
689

690 691 692 693 694 695 696 697 698 699 700 701 702
      def fields_for(record_or_name_or_array, *args, &block)
        case record_or_name_or_array
        when String, Symbol
          name = "#{object_name}[#{record_or_name_or_array}]"
        when Array
          object = record_or_name_or_array.last
          name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
          args.unshift(object)
        else
          object = record_or_name_or_array
          name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
          args.unshift(object)
        end
703

704 705
        @template.fields_for(name, *args, &block)
      end
706 707

      def label(method, text = nil, options = {})
708
        @template.label(@object_name, method, text, objectify_options(options))
709
      end
710

711
      def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
712
        @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
713
      end
714

715
      def radio_button(method, tag_value, options = {})
716
        @template.radio_button(@object_name, method, tag_value, objectify_options(options))
717
      end
718

719
      def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError")
720
        @template.error_message_on(@object, method, prepend_text, append_text, css_class)
721
      end
722 723

      def error_messages(options = {})
724
        @template.error_messages_for(@object_name, objectify_options(options))
725
      end
726

727 728 729
      def submit(value = "Save changes", options = {})
        @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit"))
      end
730 731 732 733 734

      private
        def objectify_options(options)
          @default_options.merge(options.merge(:object => @object))
        end
735
    end
D
Initial  
David Heinemeier Hansson 已提交
736
  end
737 738 739 740 741

  class Base
    cattr_accessor :default_form_builder
    self.default_form_builder = ::ActionView::Helpers::FormBuilder
  end
742
end