form_helper.rb 20.1 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2 3 4 5 6 7 8 9 10
require 'cgi'
require File.dirname(__FILE__) + '/date_helper'
require File.dirname(__FILE__) + '/tag_helper'

module ActionView
  module Helpers
    # Provides a set of methods for working with forms and especially forms related to objects assigned to the template.
    # The following is an example of a complete form for a person object that works for both creates and updates built
    # with all the form helpers. The <tt>@person</tt> object was assigned by an action on the controller:
    #   <form action="save_person" method="post">
11
    #     Name:
D
Initial  
David Heinemeier Hansson 已提交
12 13
    #     <%= text_field "person", "name", "size" => 20 %>
    #
14
    #     Password:
D
Initial  
David Heinemeier Hansson 已提交
15 16 17 18 19 20 21 22 23 24 25 26 27 28
    #     <%= password_field "person", "password", "maxsize" => 20 %>
    #
    #     Single?:
    #     <%= check_box "person", "single" %>
    #
    #     Description:
    #     <%= text_area "person", "description", "cols" => 20 %>
    #
    #     <input type="submit" value="Save">
    #   </form>
    #
    # ...is compiled to:
    #
    #   <form action="save_person" method="post">
29 30
    #     Name:
    #     <input type="text" id="person_name" name="person[name]"
D
Initial  
David Heinemeier Hansson 已提交
31 32
    #       size="20" value="<%= @person.name %>" />
    #
33 34
    #     Password:
    #     <input type="password" id="person_password" name="person[password]"
D
Initial  
David Heinemeier Hansson 已提交
35 36 37
    #       size="20" maxsize="20" value="<%= @person.password %>" />
    #
    #     Single?:
38
    #     <input type="checkbox" id="person_single" name="person[single]" value="1" />
D
Initial  
David Heinemeier Hansson 已提交
39 40 41 42 43 44 45
    #
    #     Description:
    #     <textarea cols="20" rows="40" id="person_description" name="person[description]">
    #       <%= @person.description %>
    #     </textarea>
    #
    #     <input type="submit" value="Save">
46
    #   </form>
D
Initial  
David Heinemeier Hansson 已提交
47
    #
48 49
    # If the object name contains square brackets the id for the object will be inserted. Example:
    #
50
    #   <%= text_field "person[]", "name" %> 
51 52 53 54 55
    # 
    # ...becomes:
    #
    #   <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" />
    #
56 57 58 59 60 61
    # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial
    # used by render_collection_of_partials, the "index" option may come in handy. Example:
    #
    #   <%= text_field "person", "name", "index" => 1 %>
    #
    # becomes
62
    #
63 64
    #   <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" />
    #
65
    # There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
D
Initial  
David Heinemeier Hansson 已提交
66 67
    # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
    module FormHelper
68 69 70
      # Creates a form and a scope around a specific model object, which is then used as a base for questioning about
      # values for the fields. Examples:
      #
N
Nicholas Seckar 已提交
71
      #   <% form_for :person, @person, :url => { :action => "update" } do |f| %>
72 73 74 75 76 77 78 79 80 81 82 83 84 85
      #     First name: <%= f.text_field :first_name %>
      #     Last name : <%= f.text_field :last_name %>
      #     Biography : <%= f.text_area :biography %>
      #     Admin?    : <%= f.check_box :admin %>
      #   <% end %>
      #
      # Worth noting is that the form_for tag is called in a ERb evaluation block, not a ERb output block. So that's <tt><% %></tt>, 
      # not <tt><%= %></tt>. Also worth noting is that the form_for yields a form_builder object, in this example as f, which emulates
      # the API for the stand-alone FormHelper methods, but without the object name. So instead of <tt>text_field :person, :name</tt>,
      # you get away with <tt>f.text_field :name</tt>. 
      #
      # That in itself is a modest increase in comfort. The big news is that form_for allows us to more easily escape the instance
      # variable convention, so while the stand-alone approach would require <tt>text_field :person, :name, :object => person</tt> 
      # to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with 
86
      # <tt>:person, person</tt> and all subsequent field calls save <tt>:person</tt> and <tt>:object => person</tt>.
87 88 89 90
      #
      # 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. Example:
      #
91
      #   <% form_for :person, @person, :url => { :action => "update" } do |f| %>
92 93 94 95 96 97 98 99
      #     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 %>
      #
      # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base.
      # Like collection_select and datetime_select.
100
      #
101 102 103 104 105 106
      # Html attributes for the form tag can be given as :html => {...}. Example:
      #     
      #   <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %>
      #     ...
      #   <% end %>
      #
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
      # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
      # then use your custom builder like so:
      #   
      #   <% 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 %>
      # 
      # In many cases you will want to wrap the above in another helper, such as:
      #
      #   def labelled_form_for(name, object, options, &proc)
      #     form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc)
      #   end
      #
123
      def form_for(object_name, *args, &proc)
124
        raise ArgumentError, "Missing block" unless block_given?
125
        options = args.last.is_a?(Hash) ? args.pop : {}
126
        concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding)
127
        fields_for(object_name, *(args << options), &proc)
128
        concat('</form>', proc.binding)
129 130 131 132 133
      end

      # 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. Example:
      #
134
      #   <% form_for :person, @person, :url => { :action => "update" } do |person_form| %>
135 136 137
      #     First name: <%= person_form.text_field :first_name %>
      #     Last name : <%= person_form.text_field :last_name %>
      #     
138
      #     <% fields_for :permission, @person.permission do |permission_fields| %>
139 140 141 142 143 144
      #       Admin?  : <%= permission_fields.check_box :admin %>
      #     <% end %>
      #   <% end %>
      #
      # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base.
      # Like collection_select and datetime_select.
145
      def fields_for(object_name, *args, &block)
146
        raise ArgumentError, "Missing block" unless block_given?
147 148
        options = args.last.is_a?(Hash) ? args.pop : {}
        object  = args.first
149 150 151

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

D
Initial  
David Heinemeier Hansson 已提交
154 155 156 157 158 159 160
      # 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
      # hash with +options+.
      #
      # Examples (call, result):
      #   text_field("post", "title", "size" => 20)
      #     <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
161 162
      def text_field(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options)
D
Initial  
David Heinemeier Hansson 已提交
163 164
      end

165
      # Works just like text_field, but returns an input tag of the "password" type instead.
166 167
      def password_field(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options)
D
Initial  
David Heinemeier Hansson 已提交
168 169
      end

170
      # Works just like text_field, but returns an input tag of the "hidden" type instead.
171
      def hidden_field(object_name, method, options = {})
172
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options)
D
Initial  
David Heinemeier Hansson 已提交
173 174
      end

175
      # Works just like text_field, but returns an input tag of the "file" type instead, which won't have a default value.
176 177
      def file_field(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options)
178 179
      end

D
Initial  
David Heinemeier Hansson 已提交
180 181 182 183 184 185 186 187 188
      # 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+.
      #
      # Example (call, result):
      #   text_area("post", "body", "cols" => 20, "rows" => 40)
      #     <textarea cols="20" rows="40" id="post_body" name="post[body]">
      #       #{@post.body}
      #     </textarea>
189 190
      def text_area(object_name, method, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options)
D
Initial  
David Heinemeier Hansson 已提交
191
      end
192

D
Initial  
David Heinemeier Hansson 已提交
193 194 195 196
      # 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+
197
      # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything.
D
Initial  
David Heinemeier Hansson 已提交
198 199 200 201
      # We work around this problem by adding a hidden value with the same name as the checkbox.
      #
      # Example (call, result). Imagine that @post.validated? returns 1:
      #   check_box("post", "validated")
202
      #     <input type="checkbox" id="post_validate" name="post[validated]" value="1" checked="checked" />
D
David Heinemeier Hansson 已提交
203
      #     <input name="post[validated]" type="hidden" value="0" />
D
Initial  
David Heinemeier Hansson 已提交
204 205 206
      #
      # Example (call, result). Imagine that @puppy.gooddog returns no:
      #   check_box("puppy", "gooddog", {}, "yes", "no")
207
      #     <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
D
David Heinemeier Hansson 已提交
208
      #     <input name="puppy[gooddog]" type="hidden" value="no" />
209 210
      def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
211
      end
212 213 214 215

      # 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
216
      # hash with +options+.
217 218 219
      # Example (call, result). Imagine that @post.category returns "rails":
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
220 221
      #     <input type="radio" id="post_category" name="post[category]" value="rails" checked="checked" />
      #     <input type="radio" id="post_category" name="post[category]" value="java" />
222
      #
223 224
      def radio_button(object_name, method, tag_value, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options)
225
      end
D
Initial  
David Heinemeier Hansson 已提交
226 227 228 229 230 231
    end

    class InstanceTag #:nodoc:
      include Helpers::TagHelper

      attr_reader :method_name, :object_name
232 233

      DEFAULT_FIELD_OPTIONS     = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS)
234
      DEFAULT_RADIO_OPTIONS     = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS)
235
      DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS)
236
      DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS)
D
Initial  
David Heinemeier Hansson 已提交
237

238
      def initialize(object_name, method_name, template_object, local_binding = nil, object = nil)
239
        @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
D
Initial  
David Heinemeier Hansson 已提交
240
        @template_object, @local_binding = template_object, local_binding
241
        @object = object
242
        if @object_name.sub!(/\[\]$/,"")
243 244 245 246 247
          if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:id_before_type_cast)
            @auto_index = object.id_before_type_cast
          else
            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to id_before_type_cast: #{object.inspect}"
          end
248
        end
D
Initial  
David Heinemeier Hansson 已提交
249
      end
250

D
Initial  
David Heinemeier Hansson 已提交
251
      def to_input_field_tag(field_type, options = {})
252
        options = options.stringify_keys
253 254
        options["size"] ||= options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"]
        options = DEFAULT_FIELD_OPTIONS.merge(options)
255 256 257 258
        if field_type == "hidden"
          options.delete("size")
        end
        options["type"] = field_type
259
        options["value"] ||= value_before_type_cast(object) unless field_type == "file"
260 261 262 263 264
        add_default_name_and_id(options)
        tag("input", options)
      end

      def to_radio_button_tag(tag_value, options = {})
265
        options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
266 267
        options["type"]     = "radio"
        options["value"]    = tag_value
268 269 270 271 272 273
        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
274
        options["checked"]  = "checked" if checked
275
        pretty_tag_value    = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
276
        options["id"]     ||= defined?(@auto_index) ?             
277 278
          "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
          "#{@object_name}_#{@method_name}_#{pretty_tag_value}"
279 280 281 282
        add_default_name_and_id(options)
        tag("input", options)
      end

D
Initial  
David Heinemeier Hansson 已提交
283
      def to_text_area_tag(options = {})
284
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
D
Initial  
David Heinemeier Hansson 已提交
285
        add_default_name_and_id(options)
286 287 288 289 290

        if size = options.delete("size")
          options["cols"], options["rows"] = size.split("x")
        end

291
        content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
D
Initial  
David Heinemeier Hansson 已提交
292 293 294
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
295 296 297
        options = options.stringify_keys
        options["type"]     = "checkbox"
        options["value"]    = checked_value
298 299 300
        if options.has_key?("checked")
          cv = options.delete "checked"
          checked = cv == true || cv == "checked"
301
        else
302
          checked = self.class.check_box_checked?(value(object), checked_value)
303
        end
304
        options["checked"] = "checked" if checked
D
Initial  
David Heinemeier Hansson 已提交
305
        add_default_name_and_id(options)
306
        tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
307 308 309
      end

      def to_date_tag()
310
        defaults = DEFAULT_DATE_OPTIONS.dup
311
        date     = value(object) || Date.today
312
        options  = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
D
Initial  
David Heinemeier Hansson 已提交
313
        html_day_select(date, options.call(3)) +
314
        html_month_select(date, options.call(2)) +
D
Initial  
David Heinemeier Hansson 已提交
315 316 317 318
        html_year_select(date, options.call(1))
      end

      def to_boolean_select_tag(options = {})
319
        options = options.stringify_keys
D
Initial  
David Heinemeier Hansson 已提交
320
        add_default_name_and_id(options)
321
        value = value(object)
D
Initial  
David Heinemeier Hansson 已提交
322 323 324 325 326 327 328 329
        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
330 331
      
      def to_content_tag(tag_name, options = {})
332
        content_tag(tag_name, value(object), options)
333 334
      end
      
D
Initial  
David Heinemeier Hansson 已提交
335
      def object
336
        @object || @template_object.instance_variable_get("@#{@object_name}")
D
Initial  
David Heinemeier Hansson 已提交
337 338
      end

339 340
      def value(object)
        self.class.value(object, @method_name)
D
Initial  
David Heinemeier Hansson 已提交
341 342
      end

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
      def value_before_type_cast(object)
        self.class.value_before_type_cast(object, @method_name)
      end
      
      class << self
        def value(object, method_name)
          object.send method_name unless object.nil?
        end
        
        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
        
        def check_box_checked?(value, checked_value)
          case value
          when TrueClass, FalseClass
            value
          when NilClass
            false
          when Integer
            value != 0
          when String
            value == checked_value
          else
            value.to_i != 0
          end
        end
        
        def radio_button_checked?(value, checked_value)
          value.to_s == checked_value.to_s
377
        end
378 379
      end

D
Initial  
David Heinemeier Hansson 已提交
380 381
      private
        def add_default_name_and_id(options)
382 383 384
          if options.has_key?("index")
            options["name"] ||= tag_name_with_index(options["index"])
            options["id"]   ||= tag_id_with_index(options["index"])
385
            options.delete("index")
386
          elsif defined?(@auto_index)
387 388
            options["name"] ||= tag_name_with_index(@auto_index)
            options["id"]   ||= tag_id_with_index(@auto_index)
389
          else
390 391
            options["name"] ||= tag_name
            options["id"]   ||= tag_id
392
          end
D
Initial  
David Heinemeier Hansson 已提交
393
        end
394

D
Initial  
David Heinemeier Hansson 已提交
395 396 397
        def tag_name
          "#{@object_name}[#{@method_name}]"
        end
398

399 400 401
        def tag_name_with_index(index)
          "#{@object_name}[#{index}][#{@method_name}]"
        end
D
Initial  
David Heinemeier Hansson 已提交
402 403

        def tag_id
404
          "#{sanitized_object_name}_#{@method_name}"
D
Initial  
David Heinemeier Hansson 已提交
405
        end
406 407

        def tag_id_with_index(index)
408 409 410 411 412
          "#{sanitized_object_name}_#{index}_#{@method_name}"
        end

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

416
    class FormBuilder #:nodoc:
417 418 419
      # The methods which wrap a form helper call.
      class_inheritable_accessor :field_helpers
      self.field_helpers = (FormHelper.instance_methods - ['form_for'])
420

421
      attr_accessor :object_name, :object, :options
422

423 424
      def initialize(object_name, object, template, options, proc)
        @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc        
425
      end
426
      
427
      (field_helpers - %w(check_box radio_button fields_for)).each do |selector|
428 429 430 431 432 433 434
        src = <<-end_src
          def #{selector}(method, options = {})
            @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object))
          end
        end_src
        class_eval src, __FILE__, __LINE__
      end
435 436 437 438 439 440

      def fields_for(name, *args, &block)
        name = "#{object_name}[#{name}]"
        @template.fields_for(name, *args, &block)
      end

441 442 443 444 445
      def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
        @template.check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value)
      end
      
      def radio_button(method, tag_value, options = {})
446
        @template.radio_button(@object_name, method, tag_value, options.merge(:object => @object))
447 448
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
449
  end
450 451 452 453 454

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