form_helper.rb 33.1 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
      # Creates a form and a scope around a specific model object that is used as a base for questioning about
77
      # values for the fields.
78
      #
N
Nicholas Seckar 已提交
79
      #   <% form_for :person, @person, :url => { :action => "update" } do |f| %>
P
Pratik Naik 已提交
80
      #     <%= f.error_messages %>
81 82 83 84 85 86
      #     First name: <%= f.text_field :first_name %>
      #     Last name : <%= f.text_field :last_name %>
      #     Biography : <%= f.text_area :biography %>
      #     Admin?    : <%= f.check_box :admin %>
      #   <% end %>
      #
87
      # 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>,
88
      # not <tt><%= %></tt>. Also worth noting is that form_for yields a <tt>form_builder</tt> object, in this example as <tt>f</tt>, which emulates
89
      # the API for the stand-alone FormHelper methods, but without the object name. So instead of <tt>text_field :person, :name</tt>,
P
Pratik Naik 已提交
90 91
      # 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.
92
      #
93 94
      # 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>
95
      # to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with
96
      # <tt>:person, person</tt> and all subsequent field calls save <tt>:person</tt> and <tt>:object => person</tt>.
97 98
      #
      # Also note that form_for doesn't create an exclusive scope. It's still possible to use both the stand-alone FormHelper methods
99
      # and methods from FormTagHelper. For example:
100
      #
101
      #   <% form_for :person, @person, :url => { :action => "update" } do |f| %>
102 103 104 105 106 107
      #     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 %>
      #
108 109
      # 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.
110
      #
111
      # HTML attributes for the form tag can be given as :html => {...}. For example:
112
      #
113 114 115 116
      #   <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %>
      #     ...
      #   <% end %>
      #
117 118 119
      # The above form will then have the <tt>id</tt> attribute with the value </tt>person_form</tt>, which you can then
      # style with CSS or manipulate with JavaScript.
      #
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
      # === Relying on record identification
      #
      # In addition to manually configuring the form_for call, you can also rely on record identification, which will use
      # the conventions and named routes of that approach. Examples:
      #
      #   <% 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:
      #
      #   <% form_for :post, @post, :url => posts_path, :html => { :class => "new_post", :id => "new_post" } do |f| %>
      #     ...
      #   <% end %>
      #
      # You can also overwrite the individual conventions, like this:
      #
      #   <% form_for(@post, :url => super_post_path(@post)) do |f| %>
      #     ...
      #   <% end %>
      #
153
      # And for namespaced routes, like admin_post_url:
154 155 156 157 158
      #
      #   <% form_for([:admin, @post]) do |f| %>
      #    ...
      #   <% end %>
      #
159 160
      # === Customized form builders
      #
161
      # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
162 163
      # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs.
      #
164 165 166 167 168 169
      #   <% 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 %>
170
      #
171 172 173 174 175 176
      # 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>.
      #
177
      # In many cases you will want to wrap the above in another helper, so you could do something like the following:
178
      #
179 180
      #   def labelled_form_for(record_or_name_or_array, *args, &proc)
      #     options = args.extract_options!
181
      #     form_for(record_or_name_or_array, *(args << options.merge(:builder => LabellingFormBuilder)), &proc)
182 183
      #   end
      #
184
      # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag.
185
      def form_for(record_or_name_or_array, *args, &proc)
186
        raise ArgumentError, "Missing block" unless block_given?
187

188
        options = args.extract_options!
189

190
        case record_or_name_or_array
191
        when String, Symbol
192
          object_name = record_or_name_or_array
193
        when Array
194
          object = record_or_name_or_array.last
195
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
196
          apply_form_for_options!(record_or_name_or_array, options)
197
          args.unshift object
198
        else
199
          object = record_or_name_or_array
200
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
201
          apply_form_for_options!([object], options)
202
          args.unshift object
203 204
        end

205
        concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding)
206
        fields_for(object_name, *(args << options), &proc)
207
        concat('</form>', proc.binding)
208
      end
209

210 211
      def apply_form_for_options!(object_or_array, options) #:nodoc:
        object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array
212

213 214 215 216 217 218 219
        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

220 221
        options[:html] ||= {}
        options[:html].reverse_merge!(html_options)
222
        options[:url] ||= polymorphic_path(object_or_array)
223
      end
224 225

      # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes
226
      # fields_for suitable for specifying additional model objects in the same form:
227
      #
228
      # ==== Examples
229
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
230 231
      #     First name: <%= person_form.text_field :first_name %>
      #     Last name : <%= person_form.text_field :last_name %>
232
      #
233
      #     <% fields_for @person.permission do |permission_fields| %>
234 235 236 237
      #       Admin?  : <%= permission_fields.check_box :admin %>
      #     <% end %>
      #   <% end %>
      #
238 239 240 241 242 243 244 245 246 247 248 249
      # ...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 %>
      #
250 251
      # 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.
252
      def fields_for(record_or_name_or_array, *args, &block)
253
        raise ArgumentError, "Missing block" unless block_given?
254
        options = args.extract_options!
255 256 257 258 259 260 261 262 263 264 265 266 267

        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
268 269 270

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

273 274 275
      # 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
276
      # onto the HTML as an HTML element attribute as in the example shown.
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
      #
      # ==== 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 已提交
292 293
      # 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
294
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
D
David Heinemeier Hansson 已提交
295
      # shown.
D
Initial  
David Heinemeier Hansson 已提交
296
      #
297
      # ==== Examples
D
David Heinemeier Hansson 已提交
298
      #   text_field(:post, :title, :size => 20)
299 300 301 302 303 304 305 306 307 308 309
      #   # => <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" />
      #
310 311
      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 已提交
312 313
      end

314 315
      # 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
316
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
      # 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" />
      #
332 333
      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 已提交
334 335
      end

336 337
      # 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
338
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
339 340
      # shown.
      #
341
      # ==== Examples
342 343 344 345 346 347 348
      #   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)
349
      #   # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
350
      def hidden_field(object_name, method, options = {})
351
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options)
D
Initial  
David Heinemeier Hansson 已提交
352 353
      end

354 355
      # 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
356
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
357 358 359 360 361 362 363 364 365 366 367 368
      # 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" />
      #
369 370
      def file_field(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options)
371 372
      end

D
Initial  
David Heinemeier Hansson 已提交
373 374 375 376
      # 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+.
      #
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
      # ==== 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>
397 398
      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 已提交
399
      end
400

D
Initial  
David Heinemeier Hansson 已提交
401 402 403 404
      # 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+
405 406
      # 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 已提交
407
      #
408
      # ==== Examples
409
      #   # Let's say that @post.validated? is 1:
D
Initial  
David Heinemeier Hansson 已提交
410
      #   check_box("post", "validated")
P
Pratik Naik 已提交
411
      #   # => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
412
      #   #    <input name="post[validated]" type="hidden" value="0" />
D
Initial  
David Heinemeier Hansson 已提交
413
      #
414
      #   # Let's say that @puppy.gooddog is "no":
D
Initial  
David Heinemeier Hansson 已提交
415
      #   check_box("puppy", "gooddog", {}, "yes", "no")
416 417 418
      #   # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
      #   #    <input name="puppy[gooddog]" type="hidden" value="no" />
      #
P
Pratik Naik 已提交
419 420
      #   check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no")
      #   # => <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
421 422
      #   #    <input name="eula[accepted]" type="hidden" value="no" />
      #
423 424
      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 已提交
425
      end
426 427 428 429

      # 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
430
      # hash with +options+.
431 432 433
      #
      # ==== Examples
      #   # Let's say that @post.category returns "rails":
434 435
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
P
Pratik Naik 已提交
436 437
      #   # => <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" />
438
      #
439 440
      #   radio_button("user", "receive_newsletter", "yes")
      #   radio_button("user", "receive_newsletter", "no")
P
Pratik Naik 已提交
441 442
      #   # => <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" />
443 444
      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)
445
      end
D
Initial  
David Heinemeier Hansson 已提交
446 447 448
    end

    class InstanceTag #:nodoc:
449
      include Helpers::TagHelper, Helpers::FormTagHelper
D
Initial  
David Heinemeier Hansson 已提交
450 451

      attr_reader :method_name, :object_name
452 453

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

457
      def initialize(object_name, method_name, template_object, local_binding = nil, object = nil)
458
        @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
D
Initial  
David Heinemeier Hansson 已提交
459
        @template_object, @local_binding = template_object, local_binding
460
        @object = object
461
        if @object_name.sub!(/\[\]$/,"")
462 463
          if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
            @auto_index = object.to_param
464
          else
465
            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
466
          end
467
        end
D
Initial  
David Heinemeier Hansson 已提交
468
      end
469

470
      def to_label_tag(text = nil, options = {})
471
        options = options.stringify_keys
472 473
        name_and_id = options.dup
        add_default_name_and_id(name_and_id)
474
        options.delete("index")
475
        options["for"] ||= name_and_id["id"]
476
        content = (text.blank? ? nil : text.to_s) || method_name.humanize
477
        label_tag(name_and_id["id"], content, options)
478 479
      end

D
Initial  
David Heinemeier Hansson 已提交
480
      def to_input_field_tag(field_type, options = {})
481
        options = options.stringify_keys
482
        options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
483
        options = DEFAULT_FIELD_OPTIONS.merge(options)
484 485 486 487
        if field_type == "hidden"
          options.delete("size")
        end
        options["type"] = field_type
488
        options["value"] ||= value_before_type_cast(object) unless field_type == "file"
489 490 491 492 493
        add_default_name_and_id(options)
        tag("input", options)
      end

      def to_radio_button_tag(tag_value, options = {})
494
        options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
495 496
        options["type"]     = "radio"
        options["value"]    = tag_value
497 498 499 500 501 502
        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
503
        options["checked"]  = "checked" if checked
504
        pretty_tag_value    = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
505
        options["id"]     ||= defined?(@auto_index) ?
506 507
          "#{tag_id_with_index(@auto_index)}_#{pretty_tag_value}" :
          "#{tag_id}_#{pretty_tag_value}"
508 509 510 511
        add_default_name_and_id(options)
        tag("input", options)
      end

D
Initial  
David Heinemeier Hansson 已提交
512
      def to_text_area_tag(options = {})
513
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
D
Initial  
David Heinemeier Hansson 已提交
514
        add_default_name_and_id(options)
515 516

        if size = options.delete("size")
517
          options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
518 519
        end

520
        content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
D
Initial  
David Heinemeier Hansson 已提交
521 522 523
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
524 525 526
        options = options.stringify_keys
        options["type"]     = "checkbox"
        options["value"]    = checked_value
527 528 529
        if options.has_key?("checked")
          cv = options.delete "checked"
          checked = cv == true || cv == "checked"
530
        else
531
          checked = self.class.check_box_checked?(value(object), checked_value)
532
        end
533
        options["checked"] = "checked" if checked
D
Initial  
David Heinemeier Hansson 已提交
534
        add_default_name_and_id(options)
535
        tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
536 537 538
      end

      def to_boolean_select_tag(options = {})
539
        options = options.stringify_keys
D
Initial  
David Heinemeier Hansson 已提交
540
        add_default_name_and_id(options)
541
        value = value(object)
D
Initial  
David Heinemeier Hansson 已提交
542 543 544 545 546 547 548 549
        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
550

551
      def to_content_tag(tag_name, options = {})
552
        content_tag(tag_name, value(object), options)
553
      end
554

D
Initial  
David Heinemeier Hansson 已提交
555
      def object
556
        @object || (@template_object.instance_variable_get("@#{@object_name}") rescue nil)
D
Initial  
David Heinemeier Hansson 已提交
557 558
      end

559 560
      def value(object)
        self.class.value(object, @method_name)
D
Initial  
David Heinemeier Hansson 已提交
561 562
      end

563 564 565
      def value_before_type_cast(object)
        self.class.value_before_type_cast(object, @method_name)
      end
566

567 568 569 570
      class << self
        def value(object, method_name)
          object.send method_name unless object.nil?
        end
571

572 573 574 575 576 577 578
        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
579

580 581 582 583 584 585 586 587 588 589 590 591 592 593
        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
594

595 596
        def radio_button_checked?(value, checked_value)
          value.to_s == checked_value.to_s
597
        end
598 599
      end

D
Initial  
David Heinemeier Hansson 已提交
600 601
      private
        def add_default_name_and_id(options)
602 603 604
          if options.has_key?("index")
            options["name"] ||= tag_name_with_index(options["index"])
            options["id"]   ||= tag_id_with_index(options["index"])
605
            options.delete("index")
606
          elsif defined?(@auto_index)
607 608
            options["name"] ||= tag_name_with_index(@auto_index)
            options["id"]   ||= tag_id_with_index(@auto_index)
609
          else
610
            options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
611
            options["id"]   ||= tag_id
612
          end
D
Initial  
David Heinemeier Hansson 已提交
613
        end
614

D
Initial  
David Heinemeier Hansson 已提交
615 616 617
        def tag_name
          "#{@object_name}[#{@method_name}]"
        end
618

619 620 621
        def tag_name_with_index(index)
          "#{@object_name}[#{index}][#{@method_name}]"
        end
D
Initial  
David Heinemeier Hansson 已提交
622 623

        def tag_id
624
          "#{sanitized_object_name}_#{@method_name}"
D
Initial  
David Heinemeier Hansson 已提交
625
        end
626 627

        def tag_id_with_index(index)
628 629 630 631 632
          "#{sanitized_object_name}_#{index}_#{@method_name}"
        end

        def sanitized_object_name
          @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
633
        end
D
Initial  
David Heinemeier Hansson 已提交
634
    end
635

636
    class FormBuilder #:nodoc:
637 638 639
      # The methods which wrap a form helper call.
      class_inheritable_accessor :field_helpers
      self.field_helpers = (FormHelper.instance_methods - ['form_for'])
640

641
      attr_accessor :object_name, :object, :options
642

643
      def initialize(object_name, object, template, options, proc)
644
        @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
645
        @default_options = @options ? @options.slice(:index) : {}
646
      end
647

648
      (field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
649 650
        src = <<-end_src
          def #{selector}(method, options = {})
651
            @template.send(#{selector.inspect}, @object_name, method, objectify_options(options))
652 653 654 655
          end
        end_src
        class_eval src, __FILE__, __LINE__
      end
656

657 658 659 660 661 662 663 664 665 666 667 668 669
      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
670

671 672
        @template.fields_for(name, *args, &block)
      end
673 674

      def label(method, text = nil, options = {})
675
        @template.label(@object_name, method, text, objectify_options(options))
676
      end
677

678
      def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
679
        @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
680
      end
681

682
      def radio_button(method, tag_value, options = {})
683
        @template.radio_button(@object_name, method, tag_value, objectify_options(options))
684
      end
685

686
      def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError")
687
        @template.error_message_on(@object, method, prepend_text, append_text, css_class)
688
      end
689 690

      def error_messages(options = {})
691
        @template.error_messages_for(@object_name, objectify_options(options))
692
      end
693

694 695 696
      def submit(value = "Save changes", options = {})
        @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit"))
      end
697 698 699 700 701

      private
        def objectify_options(options)
          @default_options.merge(options.merge(:object => @object))
        end
702
    end
D
Initial  
David Heinemeier Hansson 已提交
703
  end
704 705 706 707 708

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