form_helper.rb 36.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
      #
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

      # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes
273
      # fields_for suitable for specifying additional model objects in the same form:
274
      #
275
      # ==== Examples
276
      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
277 278
      #     First name: <%= person_form.text_field :first_name %>
      #     Last name : <%= person_form.text_field :last_name %>
279
      #
280
      #     <% fields_for @person.permission do |permission_fields| %>
281 282 283 284
      #       Admin?  : <%= permission_fields.check_box :admin %>
      #     <% end %>
      #   <% end %>
      #
285 286 287 288 289 290 291 292 293 294 295 296
      # ...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 %>
      #
297 298
      # 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.
299
      def fields_for(record_or_name_or_array, *args, &block)
300
        raise ArgumentError, "Missing block" unless block_given?
301
        options = args.extract_options!
302 303 304 305 306 307 308 309 310

        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
311 312 313

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

316 317 318
      # 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
319
      # onto the HTML as an HTML element attribute as in the example shown.
320 321 322
      #
      # ==== Examples
      #   label(:post, :title)
P
Pratik Naik 已提交
323
      #   # => <label for="post_title">Title</label>
324 325
      #
      #   label(:post, :title, "A short title")
P
Pratik Naik 已提交
326
      #   # => <label for="post_title">A short title</label>
327 328
      #
      #   label(:post, :title, "A short title", :class => "title_label")
P
Pratik Naik 已提交
329
      #   # => <label for="post_title" class="title_label">A short title</label>
330 331
      #
      def label(object_name, method, text = nil, options = {})
332
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options)
333 334
      end

D
Initial  
David Heinemeier Hansson 已提交
335 336
      # 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
337
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
D
David Heinemeier Hansson 已提交
338
      # shown.
D
Initial  
David Heinemeier Hansson 已提交
339
      #
340
      # ==== Examples
D
David Heinemeier Hansson 已提交
341
      #   text_field(:post, :title, :size => 20)
342 343 344 345 346 347 348 349 350 351 352
      #   # => <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" />
      #
353
      def text_field(object_name, method, options = {})
354
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("text", options)
D
Initial  
David Heinemeier Hansson 已提交
355 356
      end

357 358
      # 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
359
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
      # 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" />
      #
375
      def password_field(object_name, method, options = {})
376
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", options)
D
Initial  
David Heinemeier Hansson 已提交
377 378
      end

379 380
      # 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
381
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
382 383
      # shown.
      #
384
      # ==== Examples
385 386 387 388 389 390 391
      #   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)
392
      #   # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
393
      def hidden_field(object_name, method, options = {})
394
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("hidden", options)
D
Initial  
David Heinemeier Hansson 已提交
395 396
      end

397 398
      # 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
399
      # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
400 401 402 403 404 405 406 407 408 409 410 411
      # 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" />
      #
412
      def file_field(object_name, method, options = {})
413
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("file", options)
414 415
      end

D
Initial  
David Heinemeier Hansson 已提交
416 417 418 419
      # 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+.
      #
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
      # ==== 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>
440
      def text_area(object_name, method, options = {})
441
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_text_area_tag(options)
D
Initial  
David Heinemeier Hansson 已提交
442
      end
443

D
Initial  
David Heinemeier Hansson 已提交
444 445 446 447
      # 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+
P
Pratik Naik 已提交
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
      # is set to 0 which is convenient for boolean values.
      #
      # ==== 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 已提交
479
      #
480
      # ==== Examples
481
      #   # Let's say that @post.validated? is 1:
D
Initial  
David Heinemeier Hansson 已提交
482
      #   check_box("post", "validated")
P
Pratik Naik 已提交
483
      #   # => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
484
      #   #    <input name="post[validated]" type="hidden" value="0" />
D
Initial  
David Heinemeier Hansson 已提交
485
      #
486
      #   # Let's say that @puppy.gooddog is "no":
D
Initial  
David Heinemeier Hansson 已提交
487
      #   check_box("puppy", "gooddog", {}, "yes", "no")
488 489 490
      #   # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
      #   #    <input name="puppy[gooddog]" type="hidden" value="no" />
      #
P
Pratik Naik 已提交
491 492
      #   check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no")
      #   # => <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
493 494
      #   #    <input name="eula[accepted]" type="hidden" value="no" />
      #
495
      def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
496
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
497
      end
498 499 500 501

      # 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
502
      # hash with +options+.
503 504 505
      #
      # ==== Examples
      #   # Let's say that @post.category returns "rails":
506 507
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
P
Pratik Naik 已提交
508 509
      #   # => <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" />
510
      #
511 512
      #   radio_button("user", "receive_newsletter", "yes")
      #   radio_button("user", "receive_newsletter", "no")
P
Pratik Naik 已提交
513 514
      #   # => <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" />
515
      def radio_button(object_name, method, tag_value, options = {})
516
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_radio_button_tag(tag_value, options)
517
      end
D
Initial  
David Heinemeier Hansson 已提交
518 519 520
    end

    class InstanceTag #:nodoc:
521
      include Helpers::TagHelper, Helpers::FormTagHelper
D
Initial  
David Heinemeier Hansson 已提交
522 523

      attr_reader :method_name, :object_name
524 525

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

529
      def initialize(object_name, method_name, template_object, object = nil)
530
        @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
531
        @template_object = template_object
532
        @object = object
533 534
        if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
          if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
535
            @auto_index = object.to_param
536
          else
537
            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
538
          end
539
        end
D
Initial  
David Heinemeier Hansson 已提交
540
      end
541

542
      def to_label_tag(text = nil, options = {})
543
        options = options.stringify_keys
544 545
        name_and_id = options.dup
        add_default_name_and_id(name_and_id)
546
        options.delete("index")
547
        options["for"] ||= name_and_id["id"]
548
        content = (text.blank? ? nil : text.to_s) || method_name.humanize
549
        label_tag(name_and_id["id"], content, options)
550 551
      end

D
Initial  
David Heinemeier Hansson 已提交
552
      def to_input_field_tag(field_type, options = {})
553
        options = options.stringify_keys
554
        options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
555
        options = DEFAULT_FIELD_OPTIONS.merge(options)
556 557 558 559
        if field_type == "hidden"
          options.delete("size")
        end
        options["type"] = field_type
560
        options["value"] ||= value_before_type_cast(object) unless field_type == "file"
561
        options["value"] &&= html_escape(options["value"])
562 563 564 565 566
        add_default_name_and_id(options)
        tag("input", options)
      end

      def to_radio_button_tag(tag_value, options = {})
567
        options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
568 569
        options["type"]     = "radio"
        options["value"]    = tag_value
570 571 572 573 574 575
        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
576
        options["checked"]  = "checked" if checked
577
        pretty_tag_value    = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
578
        options["id"]     ||= defined?(@auto_index) ?
579 580
          "#{tag_id_with_index(@auto_index)}_#{pretty_tag_value}" :
          "#{tag_id}_#{pretty_tag_value}"
581 582 583 584
        add_default_name_and_id(options)
        tag("input", options)
      end

D
Initial  
David Heinemeier Hansson 已提交
585
      def to_text_area_tag(options = {})
586
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
D
Initial  
David Heinemeier Hansson 已提交
587
        add_default_name_and_id(options)
588 589

        if size = options.delete("size")
590
          options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
591 592
        end

593
        content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
D
Initial  
David Heinemeier Hansson 已提交
594 595 596
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
597 598 599
        options = options.stringify_keys
        options["type"]     = "checkbox"
        options["value"]    = checked_value
600 601 602
        if options.has_key?("checked")
          cv = options.delete "checked"
          checked = cv == true || cv == "checked"
603
        else
604
          checked = self.class.check_box_checked?(value(object), checked_value)
605
        end
606
        options["checked"] = "checked" if checked
D
Initial  
David Heinemeier Hansson 已提交
607
        add_default_name_and_id(options)
608
        tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
609 610 611
      end

      def to_boolean_select_tag(options = {})
612
        options = options.stringify_keys
D
Initial  
David Heinemeier Hansson 已提交
613
        add_default_name_and_id(options)
614
        value = value(object)
D
Initial  
David Heinemeier Hansson 已提交
615 616 617 618 619 620 621 622
        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
623

624
      def to_content_tag(tag_name, options = {})
625
        content_tag(tag_name, value(object), options)
626
      end
627

D
Initial  
David Heinemeier Hansson 已提交
628
      def object
629 630 631 632 633
        @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 已提交
634 635
      end

636 637
      def value(object)
        self.class.value(object, @method_name)
D
Initial  
David Heinemeier Hansson 已提交
638 639
      end

640 641 642
      def value_before_type_cast(object)
        self.class.value_before_type_cast(object, @method_name)
      end
643

644 645 646 647
      class << self
        def value(object, method_name)
          object.send method_name unless object.nil?
        end
648

649 650 651 652 653 654 655
        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
656

657 658 659 660 661 662 663 664 665 666
        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
667 668
          when Array
            value.include?(checked_value)
669 670 671 672
          else
            value.to_i != 0
          end
        end
673

674 675
        def radio_button_checked?(value, checked_value)
          value.to_s == checked_value.to_s
676
        end
677 678
      end

D
Initial  
David Heinemeier Hansson 已提交
679 680
      private
        def add_default_name_and_id(options)
681 682 683
          if options.has_key?("index")
            options["name"] ||= tag_name_with_index(options["index"])
            options["id"]   ||= tag_id_with_index(options["index"])
684
            options.delete("index")
685
          elsif defined?(@auto_index)
686 687
            options["name"] ||= tag_name_with_index(@auto_index)
            options["id"]   ||= tag_id_with_index(@auto_index)
688
          else
689
            options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
690
            options["id"]   ||= tag_id
691
          end
D
Initial  
David Heinemeier Hansson 已提交
692
        end
693

D
Initial  
David Heinemeier Hansson 已提交
694
        def tag_name
695
          "#{@object_name}[#{sanitized_method_name}]"
D
Initial  
David Heinemeier Hansson 已提交
696
        end
697

698
        def tag_name_with_index(index)
699
          "#{@object_name}[#{index}][#{sanitized_method_name}]"
700
        end
D
Initial  
David Heinemeier Hansson 已提交
701 702

        def tag_id
703
          "#{sanitized_object_name}_#{sanitized_method_name}"
D
Initial  
David Heinemeier Hansson 已提交
704
        end
705 706

        def tag_id_with_index(index)
707
          "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
708 709 710
        end

        def sanitized_object_name
711
          @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
712 713 714 715
        end

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

719
    class FormBuilder #:nodoc:
720 721 722
      # The methods which wrap a form helper call.
      class_inheritable_accessor :field_helpers
      self.field_helpers = (FormHelper.instance_methods - ['form_for'])
723

724
      attr_accessor :object_name, :object, :options
725

726
      def initialize(object_name, object, template, options, proc)
727
        @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
728
        @default_options = @options ? @options.slice(:index) : {}
729 730 731 732 733 734 735
        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
736
      end
737

738
      (field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
739 740
        src = <<-end_src
          def #{selector}(method, options = {})
741
            @template.send(#{selector.inspect}, @object_name, method, objectify_options(options))
742 743 744 745
          end
        end_src
        class_eval src, __FILE__, __LINE__
      end
746

747
      def fields_for(record_or_name_or_array, *args, &block)
748 749 750 751 752 753 754 755 756
        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

757 758
        case record_or_name_or_array
        when String, Symbol
759
          name = "#{object_name}#{index}[#{record_or_name_or_array}]"
760 761
        when Array
          object = record_or_name_or_array.last
762
          name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
763 764 765
          args.unshift(object)
        else
          object = record_or_name_or_array
766
          name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
767 768
          args.unshift(object)
        end
769

770 771
        @template.fields_for(name, *args, &block)
      end
772 773

      def label(method, text = nil, options = {})
774
        @template.label(@object_name, method, text, objectify_options(options))
775
      end
776

777
      def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
778
        @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
779
      end
780

781
      def radio_button(method, tag_value, options = {})
782
        @template.radio_button(@object_name, method, tag_value, objectify_options(options))
783
      end
784

785
      def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError")
786
        @template.error_message_on(@object, method, prepend_text, append_text, css_class)
787
      end
788 789

      def error_messages(options = {})
790
        @template.error_messages_for(@object_name, objectify_options(options))
791
      end
792

793 794 795
      def submit(value = "Save changes", options = {})
        @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit"))
      end
796 797 798 799 800

      private
        def objectify_options(options)
          @default_options.merge(options.merge(:object => @object))
        end
801
    end
D
Initial  
David Heinemeier Hansson 已提交
802
  end
803 804 805 806 807

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