form_helper.rb 43.4 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
      #
P
Pratik Naik 已提交
79
      # Rails provides succinct resource-oriented form generation with +form_for+
80 81 82 83 84 85 86
      # 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 %>
      #
P
Pratik Naik 已提交
89
      # There, +form_for+ is able to generate the rest of RESTful form parameters
90 91
      # 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
      # === Generic form_for
      #
P
Pratik Naik 已提交
95
      # The generic way to call +form_for+ yields a form builder around a model:
96
      #
P
Pratik Naik 已提交
97
      #   <% form_for :person, :url => { :action => "update" } do |f| %>
98 99 100 101 102 103
      #     <%= 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
      #
P
Pratik Naik 已提交
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 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
      # There, the first argument is a symbol or string with the name of the
      # object the form is about, and also the name of the instance variable the
      # object is stored in.
      #
      # The form builder acts as a regular form helper that somehow carries the
      # model. Thus, the idea is that
      #
      #   <%= f.text_field :first_name %>
      #
      # gets expanded to
      #
      #   <%= text_field :person, :first_name %>
      #
      # If the instance variable is not <tt>@person</tt> you can pass the actual
      # record as the second argument:
      #
      #   <% form_for :person, person, :url => { :action => "update" } do |f| %>
      #     ...
      #   <% end %>
      #
      # In that case you can think
      #
      #   <%= f.text_field :first_name %>
      #
      # gets expanded to
      #
      #   <%= text_field :person, :first_name, :object => person %>
      #
      # You can even display error messages of the wrapped model this way:
      #
      #   <%= f.error_messages %>
      #
      # In any of its variants, the rightmost argument to +form_for+ is an
      # optional hash of options:
      #
      # * <tt>:url</tt> - The URL the form is submitted to. It takes the same fields
      #   you pass to +url_for+ or +link_to+. In particular you may pass here a
      #   named route directly as well. Defaults to the current action.
      # * <tt>:html</tt> - Optional HTML attributes for the form tag.
      #
145
      # Worth noting is that the +form_for+ tag is called in a ERb evaluation block,
P
Pratik Naik 已提交
146
      # not an ERb output block. So that's <tt><% %></tt>, not <tt><%= %></tt>.
147 148 149 150
      #
      # 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:
151
      #
152
      #   <% form_for :person, @person, :url => { :action => "update" } do |f| %>
153 154 155 156 157 158
      #     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 %>
      #
159 160 161
      # 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.
162
      #
P
Pratik Naik 已提交
163
      # === Resource-oriented style
164
      #
165
      # As we said above, in addition to manually configuring the +form_for+ call,
P
Pratik Naik 已提交
166 167 168 169 170
      # you can rely on automated resource identification, which will use the conventions
      # and named routes of that approach. This is the preferred way to use +form_for+
      # nowadays.
      #
      # For example, if <tt>@post</tt> is an existing record you want to edit
171
      #
P
Pratik Naik 已提交
172
      #   <% form_for @post do |f| %>
173 174 175
      #     ...
      #   <% end %>
      #
P
Pratik Naik 已提交
176
      # is equivalent to something like:
177 178 179 180 181
      #
      #   <% form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
      #     ...
      #   <% end %>
      #
P
Pratik Naik 已提交
182
      # And for new records
183 184 185 186 187
      #
      #   <% form_for(Post.new) do |f| %>
      #     ...
      #   <% end %>
      #
P
Pratik Naik 已提交
188
      # expands to
189
      #
190
      #   <% form_for :post, Post.new, :url => posts_path, :html => { :class => "new_post", :id => "new_post" } do |f| %>
191 192 193 194 195 196 197 198 199
      #     ...
      #   <% end %>
      #
      # You can also overwrite the individual conventions, like this:
      #
      #   <% form_for(@post, :url => super_post_path(@post)) do |f| %>
      #     ...
      #   <% end %>
      #
200
      # And for namespaced routes, like +admin_post_url+:
201 202 203 204 205
      #
      #   <% form_for([:admin, @post]) do |f| %>
      #    ...
      #   <% end %>
      #
206 207
      # === Customized form builders
      #
208
      # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
209 210
      # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs.
      #
211 212 213 214 215 216
      #   <% 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 %>
217
      #
218 219 220 221 222 223
      # 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>.
      #
224
      # In many cases you will want to wrap the above in another helper, so you could do something like the following:
225
      #
226 227
      #   def labelled_form_for(record_or_name_or_array, *args, &proc)
      #     options = args.extract_options!
228
      #     form_for(record_or_name_or_array, *(args << options.merge(:builder => LabellingFormBuilder)), &proc)
229 230
      #   end
      #
231
      # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag.
232
      def form_for(record_or_name_or_array, *args, &proc)
233
        raise ArgumentError, "Missing block" unless block_given?
234

235
        options = args.extract_options!
236

237
        case record_or_name_or_array
238
        when String, Symbol
239
          object_name = record_or_name_or_array
240
        when Array
241
          object = record_or_name_or_array.last
242
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
243
          apply_form_for_options!(record_or_name_or_array, options)
244
          args.unshift object
245
        else
246
          object = record_or_name_or_array
247
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
248
          apply_form_for_options!([object], options)
249
          args.unshift object
250 251
        end

252
        concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}))
253
        fields_for(object_name, *(args << options), &proc)
254
        concat('</form>')
255
      end
256

257 258
      def apply_form_for_options!(object_or_array, options) #:nodoc:
        object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array
259

260 261 262 263 264 265 266
        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

267 268
        options[:html] ||= {}
        options[:html].reverse_merge!(html_options)
269
        options[:url] ||= polymorphic_path(object_or_array)
270
      end
271

272 273 274 275 276
      # Creates a scope around a specific model object like form_for, but
      # doesn't create the form tags themselves. This makes fields_for suitable
      # for specifying additional model objects in the same form.
      #
      # === Generic Examples
277
      #
278
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
279 280
      #     First name: <%= person_form.text_field :first_name %>
      #     Last name : <%= person_form.text_field :last_name %>
281
      #
282
      #     <% fields_for @person.permission do |permission_fields| %>
283 284 285 286
      #       Admin?  : <%= permission_fields.check_box :admin %>
      #     <% end %>
      #   <% end %>
      #
287 288
      # ...or if you have an object that needs to be represented as a different
      # parameter, like a Client that acts as a Person:
289 290 291 292 293
      #
      #   <% fields_for :person, @client do |permission_fields| %>
      #     Admin?: <%= permission_fields.check_box :admin %>
      #   <% end %>
      #
294
      # ...or if you don't have an object, just a name of the parameter:
295 296 297 298 299
      #
      #   <% fields_for :person do |permission_fields| %>
      #     Admin?: <%= permission_fields.check_box :admin %>
      #   <% end %>
      #
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
      # 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.
      #
      # === Nested Attributes Examples
      #
      # When the object belonging to the current scope has a nested attribute
      # writer for a certain attribute, fields_for will yield a new scope
      # for that attribute. This allows you to create forms that set or change
      # the attributes of a parent object and its associations in one go.
      #
      # Nested attribute writers are normal setter methods named after an
      # association. The most common way of defining these writers is either
      # with +accepts_nested_attributes_for+ in a model definition or by
      # defining a method with the proper name. For example: the attribute
      # writer for the association <tt>:address</tt> is called
      # <tt>address_attributes=</tt>.
      #
      # Whether a one-to-one or one-to-many style form builder will be yielded
      # depends on whether the normal reader method returns a _single_ object
      # or an _array_ of objects.
      #
      # ==== One-to-one
      #
      # Consider a Person class which returns a _single_ Address from the
      # <tt>address</tt> reader method and responds to the
      # <tt>address_attributes=</tt> writer method:
      #
      #   class Person
      #     def address
      #       @address
      #     end
      #
      #     def address_attributes=(attributes)
      #       # Process the attributes hash
      #     end
      #   end
      #
      # This model can now be used with a nested fields_for, like so:
      #
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
      #     ...
      #     <% person_form.fields_for :address do |address_fields| %>
      #       Street  : <%= address_fields.text_field :street %>
      #       Zip code: <%= address_fields.text_field :zip_code %>
      #     <% end %>
      #   <% end %>
      #
      # When address is already an association on a Person you can use
      # +accepts_nested_attributes_for+ to define the writer method for you:
      #
      #   class Person < ActiveRecord::Base
      #     has_one :address
      #     accepts_nested_attributes_for :address
      #   end
      #
      # If you want to destroy the associated model through the form, you have
      # to enable it first using the <tt>:allow_destroy</tt> option for
      # +accepts_nested_attributes_for+:
      #
      #   class Person < ActiveRecord::Base
      #     has_one :address
      #     accepts_nested_attributes_for :address, :allow_destroy => true
      #   end
      #
      # Now, when you use a form element with the <tt>_delete</tt> parameter,
      # with a value that evaluates to +true+, you will destroy the associated
      # model (eg. 1, '1', true, or 'true'):
      #
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
      #     ...
      #     <% person_form.fields_for :address do |address_fields| %>
      #       ...
      #       Delete: <%= address_fields.check_box :_delete %>
      #     <% end %>
      #   <% end %>
      #
      # ==== One-to-many
      #
      # Consider a Person class which returns an _array_ of Project instances
      # from the <tt>projects</tt> reader method and responds to the
      # <tt>projects_attributes=</tt> writer method:
      #
      #   class Person
      #     def projects
      #       [@project1, @project2]
      #     end
      #
      #     def projects_attributes=(attributes)
      #       # Process the attributes hash
      #     end
      #   end
      #
      # This model can now be used with a nested fields_for. The block given to
      # the nested fields_for call will be repeated for each instance in the
      # collection:
      #
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
      #     ...
      #     <% person_form.fields_for :projects do |project_fields| %>
      #       <% if project_fields.object.active? %>
      #         Name: <%= project_fields.text_field :name %>
      #       <% end %>
      #     <% end %>
      #   <% end %>
      #
      # It's also possible to specify the instance to be used:
      #
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
      #     ...
      #     <% @person.projects.each do |project| %>
      #       <% if project.active? %>
      #         <% person_form.fields_for :projects, project do |project_fields| %>
      #           Name: <%= project_fields.text_field :name %>
      #         <% end %>
      #       <% end %>
      #     <% end %>
      #   <% end %>
      #
      # When projects is already an association on Person you can use
      # +accepts_nested_attributes_for+ to define the writer method for you:
      #
      #   class Person < ActiveRecord::Base
      #     has_many :projects
      #     accepts_nested_attributes_for :projects
      #   end
      #
      # If you want to destroy any of the associated models through the
      # form, you have to enable it first using the <tt>:allow_destroy</tt>
      # option for +accepts_nested_attributes_for+:
      #
      #   class Person < ActiveRecord::Base
      #     has_many :projects
      #     accepts_nested_attributes_for :projects, :allow_destroy => true
      #   end
      #
      # This will allow you to specify which models to destroy in the
      # attributes hash by adding a form element for the <tt>_delete</tt>
      # parameter with a value that evaluates to +true+
      # (eg. 1, '1', true, or 'true'):
      #
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
      #     ...
      #     <% person_form.fields_for :projects do |project_fields| %>
      #       Delete: <%= project_fields.check_box :_delete %>
      #     <% end %>
      #   <% end %>
447
      def fields_for(record_or_name_or_array, *args, &block)
448
        raise ArgumentError, "Missing block" unless block_given?
449
        options = args.extract_options!
450 451 452 453 454 455 456 457 458

        case record_or_name_or_array
        when String, Symbol
          object_name = record_or_name_or_array
          object = args.first
        else
          object = record_or_name_or_array
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
        end
459 460 461

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

464 465 466
      # 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
467
      # onto the HTML as an HTML element attribute as in the example shown.
468 469 470
      #
      # ==== Examples
      #   label(:post, :title)
P
Pratik Naik 已提交
471
      #   # => <label for="post_title">Title</label>
472 473
      #
      #   label(:post, :title, "A short title")
P
Pratik Naik 已提交
474
      #   # => <label for="post_title">A short title</label>
475 476
      #
      #   label(:post, :title, "A short title", :class => "title_label")
P
Pratik Naik 已提交
477
      #   # => <label for="post_title" class="title_label">A short title</label>
478 479
      #
      def label(object_name, method, text = nil, options = {})
480
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options)
481 482
      end

D
Initial  
David Heinemeier Hansson 已提交
483 484
      # 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
485
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
D
David Heinemeier Hansson 已提交
486
      # shown.
D
Initial  
David Heinemeier Hansson 已提交
487
      #
488
      # ==== Examples
D
David Heinemeier Hansson 已提交
489
      #   text_field(:post, :title, :size => 20)
490 491 492 493 494 495 496 497 498 499 500
      #   # => <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" />
      #
501
      def text_field(object_name, method, options = {})
502
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("text", options)
D
Initial  
David Heinemeier Hansson 已提交
503 504
      end

505 506
      # 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
507
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
      # 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" />
      #
523
      def password_field(object_name, method, options = {})
524
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", options)
D
Initial  
David Heinemeier Hansson 已提交
525 526
      end

527 528
      # 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
529
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
530 531
      # shown.
      #
532
      # ==== Examples
533 534 535 536 537 538 539
      #   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)
540
      #   # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
541
      def hidden_field(object_name, method, options = {})
542
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("hidden", options)
D
Initial  
David Heinemeier Hansson 已提交
543 544
      end

545 546
      # 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
547
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
548 549 550 551 552 553 554 555 556 557 558 559
      # 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" />
      #
560
      def file_field(object_name, method, options = {})
561
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("file", options)
562 563
      end

D
Initial  
David Heinemeier Hansson 已提交
564 565 566 567
      # 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+.
      #
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
      # ==== 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>
588
      def text_area(object_name, method, options = {})
589
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_text_area_tag(options)
D
Initial  
David Heinemeier Hansson 已提交
590
      end
591

D
Initial  
David Heinemeier Hansson 已提交
592
      # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
P
Pratik Naik 已提交
593 594 595 596
      # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local 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+ is set to 0 which is convenient for boolean values.
P
Pratik Naik 已提交
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
      #
      # ==== Gotcha
      #
      # The HTML specification says unchecked check boxes are not successful, and
      # thus web browsers do not send them. Unfortunately this introduces a gotcha:
      # if an Invoice model has a +paid+ flag, and in the form that edits a paid
      # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
      # any mass-assignment idiom like
      #
      #   @invoice.update_attributes(params[:invoice])
      #
      # wouldn't update the flag.
      #
      # To prevent this the helper generates a hidden field with the same name as
      # the checkbox after the very check box. So, the client either sends only the
      # hidden field (representing the check box is unchecked), or both fields.
      # Since the HTML specification says key/value pairs have to be sent in the
      # same order they appear in the form and Rails parameters extraction always
      # gets the first occurrence of any given key, that works in ordinary forms.
      #
      # Unfortunately that workaround does not work when the check box goes
      # within an array-like parameter, as in
      #
      #   <% fields_for "project[invoice_attributes][]", invoice, :index => nil do |form| %>
      #     <%= form.check_box :paid %>
      #     ...
      #   <% end %>
      #
      # because parameter name repetition is precisely what Rails seeks to distinguish
      # the elements of the array.
D
Initial  
David Heinemeier Hansson 已提交
627
      #
628
      # ==== Examples
629
      #   # Let's say that @post.validated? is 1:
D
Initial  
David Heinemeier Hansson 已提交
630
      #   check_box("post", "validated")
P
Pratik Naik 已提交
631
      #   # => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
632
      #   #    <input name="post[validated]" type="hidden" value="0" />
D
Initial  
David Heinemeier Hansson 已提交
633
      #
634
      #   # Let's say that @puppy.gooddog is "no":
D
Initial  
David Heinemeier Hansson 已提交
635
      #   check_box("puppy", "gooddog", {}, "yes", "no")
636 637 638
      #   # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
      #   #    <input name="puppy[gooddog]" type="hidden" value="no" />
      #
P
Pratik Naik 已提交
639 640
      #   check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no")
      #   # => <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
641 642
      #   #    <input name="eula[accepted]" type="hidden" value="no" />
      #
643
      def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
644
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
645
      end
646 647 648 649

      # 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
650
      # hash with +options+.
651 652 653
      #
      # ==== Examples
      #   # Let's say that @post.category returns "rails":
654 655
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
P
Pratik Naik 已提交
656 657
      #   # => <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" />
658
      #
659 660
      #   radio_button("user", "receive_newsletter", "yes")
      #   radio_button("user", "receive_newsletter", "no")
P
Pratik Naik 已提交
661 662
      #   # => <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" />
663
      def radio_button(object_name, method, tag_value, options = {})
664
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_radio_button_tag(tag_value, options)
665
      end
D
Initial  
David Heinemeier Hansson 已提交
666 667 668
    end

    class InstanceTag #:nodoc:
669
      include Helpers::TagHelper, Helpers::FormTagHelper
D
Initial  
David Heinemeier Hansson 已提交
670 671

      attr_reader :method_name, :object_name
672 673

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

677
      def initialize(object_name, method_name, template_object, object = nil)
678
        @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
679
        @template_object = template_object
680
        @object = object
681 682
        if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
          if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
683
            @auto_index = object.to_param
684
          else
685
            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
686
          end
687
        end
D
Initial  
David Heinemeier Hansson 已提交
688
      end
689

690
      def to_label_tag(text = nil, options = {})
691
        options = options.stringify_keys
692 693
        name_and_id = options.dup
        add_default_name_and_id(name_and_id)
694
        options.delete("index")
695
        options["for"] ||= name_and_id["id"]
696
        content = (text.blank? ? nil : text.to_s) || method_name.humanize
697
        label_tag(name_and_id["id"], content, options)
698 699
      end

D
Initial  
David Heinemeier Hansson 已提交
700
      def to_input_field_tag(field_type, options = {})
701
        options = options.stringify_keys
702
        options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
703
        options = DEFAULT_FIELD_OPTIONS.merge(options)
704 705 706 707
        if field_type == "hidden"
          options.delete("size")
        end
        options["type"] = field_type
708
        options["value"] ||= value_before_type_cast(object) unless field_type == "file"
709
        options["value"] &&= html_escape(options["value"])
710 711 712 713 714
        add_default_name_and_id(options)
        tag("input", options)
      end

      def to_radio_button_tag(tag_value, options = {})
715
        options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
716 717
        options["type"]     = "radio"
        options["value"]    = tag_value
718 719 720 721 722 723
        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
724
        options["checked"]  = "checked" if checked
725
        pretty_tag_value    = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
726
        options["id"]     ||= defined?(@auto_index) ?
727 728
          "#{tag_id_with_index(@auto_index)}_#{pretty_tag_value}" :
          "#{tag_id}_#{pretty_tag_value}"
729 730 731 732
        add_default_name_and_id(options)
        tag("input", options)
      end

D
Initial  
David Heinemeier Hansson 已提交
733
      def to_text_area_tag(options = {})
734
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
D
Initial  
David Heinemeier Hansson 已提交
735
        add_default_name_and_id(options)
736 737

        if size = options.delete("size")
738
          options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
739 740
        end

741
        content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
D
Initial  
David Heinemeier Hansson 已提交
742 743 744
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
745 746 747
        options = options.stringify_keys
        options["type"]     = "checkbox"
        options["value"]    = checked_value
748 749 750
        if options.has_key?("checked")
          cv = options.delete "checked"
          checked = cv == true || cv == "checked"
751
        else
752
          checked = self.class.check_box_checked?(value(object), checked_value)
753
        end
754
        options["checked"] = "checked" if checked
D
Initial  
David Heinemeier Hansson 已提交
755
        add_default_name_and_id(options)
756
        tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
757 758 759
      end

      def to_boolean_select_tag(options = {})
760
        options = options.stringify_keys
D
Initial  
David Heinemeier Hansson 已提交
761
        add_default_name_and_id(options)
762
        value = value(object)
D
Initial  
David Heinemeier Hansson 已提交
763 764 765 766 767 768 769 770
        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
771

772
      def to_content_tag(tag_name, options = {})
773
        content_tag(tag_name, value(object), options)
774
      end
775

D
Initial  
David Heinemeier Hansson 已提交
776
      def object
777 778 779 780 781
        @object || @template_object.instance_variable_get("@#{@object_name}")
      rescue NameError
        # As @object_name may contain the nested syntax (item[subobject]) we
        # need to fallback to nil.
        nil
D
Initial  
David Heinemeier Hansson 已提交
782 783
      end

784 785
      def value(object)
        self.class.value(object, @method_name)
D
Initial  
David Heinemeier Hansson 已提交
786 787
      end

788 789 790
      def value_before_type_cast(object)
        self.class.value_before_type_cast(object, @method_name)
      end
791

792 793 794 795
      class << self
        def value(object, method_name)
          object.send method_name unless object.nil?
        end
796

797 798 799 800 801 802 803
        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
804

805 806 807 808 809 810 811 812 813 814
        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
815 816
          when Array
            value.include?(checked_value)
817 818 819 820
          else
            value.to_i != 0
          end
        end
821

822 823
        def radio_button_checked?(value, checked_value)
          value.to_s == checked_value.to_s
824
        end
825 826
      end

D
Initial  
David Heinemeier Hansson 已提交
827 828
      private
        def add_default_name_and_id(options)
829 830 831
          if options.has_key?("index")
            options["name"] ||= tag_name_with_index(options["index"])
            options["id"]   ||= tag_id_with_index(options["index"])
832
            options.delete("index")
833
          elsif defined?(@auto_index)
834 835
            options["name"] ||= tag_name_with_index(@auto_index)
            options["id"]   ||= tag_id_with_index(@auto_index)
836
          else
837
            options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
838
            options["id"]   ||= tag_id
839
          end
D
Initial  
David Heinemeier Hansson 已提交
840
        end
841

D
Initial  
David Heinemeier Hansson 已提交
842
        def tag_name
843
          "#{@object_name}[#{sanitized_method_name}]"
D
Initial  
David Heinemeier Hansson 已提交
844
        end
845

846
        def tag_name_with_index(index)
847
          "#{@object_name}[#{index}][#{sanitized_method_name}]"
848
        end
D
Initial  
David Heinemeier Hansson 已提交
849 850

        def tag_id
851
          "#{sanitized_object_name}_#{sanitized_method_name}"
D
Initial  
David Heinemeier Hansson 已提交
852
        end
853 854

        def tag_id_with_index(index)
855
          "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
856 857 858
        end

        def sanitized_object_name
859
          @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
860 861 862 863
        end

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

867
    class FormBuilder #:nodoc:
868 869 870
      # The methods which wrap a form helper call.
      class_inheritable_accessor :field_helpers
      self.field_helpers = (FormHelper.instance_methods - ['form_for'])
871

872
      attr_accessor :object_name, :object, :options
873

874
      def initialize(object_name, object, template, options, proc)
875
        @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
876
        @default_options = @options ? @options.slice(:index) : {}
877 878 879 880 881 882 883
        if @object_name.to_s.match(/\[\]$/)
          if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
            @auto_index = object.to_param
          else
            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
          end
        end
884
      end
885

886
      (field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
887
        src = <<-end_src
888 889 890 891 892 893 894
          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
895 896 897
        end_src
        class_eval src, __FILE__, __LINE__
      end
898

899
      def fields_for(record_or_name_or_array, *args, &block)
900 901 902 903 904 905 906 907 908
        if options.has_key?(:index)
          index = "[#{options[:index]}]"
        elsif defined?(@auto_index)
          self.object_name = @object_name.to_s.sub(/\[\]$/,"")
          index = "[#{@auto_index}]"
        else
          index = ""
        end

909 910
        case record_or_name_or_array
        when String, Symbol
911 912 913 914 915
          if nested_attributes_association?(record_or_name_or_array)
            return fields_for_with_nested_attributes(record_or_name_or_array, args, block)
          else
            name = "#{object_name}#{index}[#{record_or_name_or_array}]"
          end
916 917
        when Array
          object = record_or_name_or_array.last
918
          name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
919 920 921
          args.unshift(object)
        else
          object = record_or_name_or_array
922
          name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
923 924
          args.unshift(object)
        end
925

926 927
        @template.fields_for(name, *args, &block)
      end
928 929

      def label(method, text = nil, options = {})
930
        @template.label(@object_name, method, text, objectify_options(options))
931
      end
932

933
      def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
934
        @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
935
      end
936

937
      def radio_button(method, tag_value, options = {})
938
        @template.radio_button(@object_name, method, tag_value, objectify_options(options))
939
      end
940

941 942
      def error_message_on(method, *args)
        @template.error_message_on(@object, method, *args)
943
      end
944 945

      def error_messages(options = {})
946
        @template.error_messages_for(@object_name, objectify_options(options))
947
      end
948

949 950 951
      def submit(value = "Save changes", options = {})
        @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit"))
      end
952 953 954 955 956

      private
        def objectify_options(options)
          @default_options.merge(options.merge(:object => @object))
        end
957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973

        def nested_attributes_association?(association_name)
          @object.respond_to?("#{association_name}_attributes=")
        end

        def fields_for_with_nested_attributes(association_name, args, block)
          name = "#{object_name}[#{association_name}_attributes]"
          association = @object.send(association_name)

          if association.is_a?(Array)
            children = args.first.respond_to?(:new_record?) ? [args.first] : association

            children.map do |child|
              child_name = "#{name}[#{ child.new_record? ? new_child_id : child.id }]"
              @template.fields_for(child_name, child, *args, &block)
            end.join
          else
974 975
            object = args.first.respond_to?(:new_record?) ? args.first : association
            @template.fields_for(name, object, *args, &block)
976 977 978 979 980 981 982 983
          end
        end

        def new_child_id
          value = (@child_counter ||= 1)
          @child_counter += 1
          "new_#{value}"
        end
984
    end
D
Initial  
David Heinemeier Hansson 已提交
985
  end
986 987 988 989 990

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