form_helper.rb 19.3 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, &proc)
146
        raise ArgumentError, "Missing block" unless block_given?
147 148
        options = args.last.is_a?(Hash) ? args.pop : {}
        object  = args.first
149
        yield((options[:builder] || FormBuilder).new(object_name, object, self, options, proc))
150 151
      end

D
Initial  
David Heinemeier Hansson 已提交
152 153 154 155 156 157 158
      # 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}" />
159 160
      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 已提交
161 162
      end

163
      # Works just like text_field, but returns an input tag of the "password" type instead.
164 165
      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 已提交
166 167
      end

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

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

D
Initial  
David Heinemeier Hansson 已提交
178 179 180 181 182 183 184 185 186
      # 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>
187 188
      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 已提交
189
      end
190

D
Initial  
David Heinemeier Hansson 已提交
191 192 193 194
      # 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+
195
      # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything.
D
Initial  
David Heinemeier Hansson 已提交
196 197 198 199
      # 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")
200
      #     <input type="checkbox" id="post_validate" name="post[validated]" value="1" checked="checked" />
D
David Heinemeier Hansson 已提交
201
      #     <input name="post[validated]" type="hidden" value="0" />
D
Initial  
David Heinemeier Hansson 已提交
202 203 204
      #
      # Example (call, result). Imagine that @puppy.gooddog returns no:
      #   check_box("puppy", "gooddog", {}, "yes", "no")
205
      #     <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
D
David Heinemeier Hansson 已提交
206
      #     <input name="puppy[gooddog]" type="hidden" value="no" />
207 208
      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 已提交
209
      end
210 211 212 213

      # 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
214
      # hash with +options+.
215 216 217
      # Example (call, result). Imagine that @post.category returns "rails":
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
218 219
      #     <input type="radio" id="post_category" name="post[category]" value="rails" checked="checked" />
      #     <input type="radio" id="post_category" name="post[category]" value="java" />
220
      #
221 222
      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)
223
      end
D
Initial  
David Heinemeier Hansson 已提交
224 225 226 227 228 229
    end

    class InstanceTag #:nodoc:
      include Helpers::TagHelper

      attr_reader :method_name, :object_name
230 231

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

236
      def initialize(object_name, method_name, template_object, local_binding = nil, object = nil)
237
        @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
D
Initial  
David Heinemeier Hansson 已提交
238
        @template_object, @local_binding = template_object, local_binding
239
        @object = object
240
        if @object_name.sub!(/\[\]$/,"")
241
          @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast
242
        end
D
Initial  
David Heinemeier Hansson 已提交
243
      end
244

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

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

D
Initial  
David Heinemeier Hansson 已提交
277
      def to_text_area_tag(options = {})
278
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
D
Initial  
David Heinemeier Hansson 已提交
279
        add_default_name_and_id(options)
280
        content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
D
Initial  
David Heinemeier Hansson 已提交
281 282 283
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
284 285 286
        options = options.stringify_keys
        options["type"]     = "checkbox"
        options["value"]    = checked_value
287 288 289
        if options.has_key?("checked")
          cv = options.delete "checked"
          checked = cv == true || cv == "checked"
290
        else
291
          checked = self.class.check_box_checked?(value(object), checked_value)
292
        end
293
        options["checked"] = "checked" if checked
D
Initial  
David Heinemeier Hansson 已提交
294
        add_default_name_and_id(options)
295
        tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
296 297 298
      end

      def to_date_tag()
299
        defaults = DEFAULT_DATE_OPTIONS.dup
300
        date     = value(object) || Date.today
301
        options  = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
D
Initial  
David Heinemeier Hansson 已提交
302
        html_day_select(date, options.call(3)) +
303
        html_month_select(date, options.call(2)) +
D
Initial  
David Heinemeier Hansson 已提交
304 305 306 307
        html_year_select(date, options.call(1))
      end

      def to_boolean_select_tag(options = {})
308
        options = options.stringify_keys
D
Initial  
David Heinemeier Hansson 已提交
309
        add_default_name_and_id(options)
310
        value = value(object)
D
Initial  
David Heinemeier Hansson 已提交
311 312 313 314 315 316 317 318
        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
319 320
      
      def to_content_tag(tag_name, options = {})
321
        content_tag(tag_name, value(object), options)
322 323
      end
      
D
Initial  
David Heinemeier Hansson 已提交
324
      def object
325
        @object || @template_object.instance_variable_get("@#{@object_name}")
D
Initial  
David Heinemeier Hansson 已提交
326 327
      end

328 329
      def value(object)
        self.class.value(object, @method_name)
D
Initial  
David Heinemeier Hansson 已提交
330 331
      end

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
      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
366
        end
367 368
      end

D
Initial  
David Heinemeier Hansson 已提交
369 370
      private
        def add_default_name_and_id(options)
371 372 373
          if options.has_key?("index")
            options["name"] ||= tag_name_with_index(options["index"])
            options["id"]   ||= tag_id_with_index(options["index"])
374
            options.delete("index")
375
          elsif @auto_index
376 377
            options["name"] ||= tag_name_with_index(@auto_index)
            options["id"]   ||= tag_id_with_index(@auto_index)
378
          else
379 380
            options["name"] ||= tag_name
            options["id"]   ||= tag_id
381
          end
D
Initial  
David Heinemeier Hansson 已提交
382
        end
383

D
Initial  
David Heinemeier Hansson 已提交
384 385 386
        def tag_name
          "#{@object_name}[#{@method_name}]"
        end
387

388 389 390
        def tag_name_with_index(index)
          "#{@object_name}[#{index}][#{@method_name}]"
        end
D
Initial  
David Heinemeier Hansson 已提交
391 392 393 394

        def tag_id
          "#{@object_name}_#{@method_name}"
        end
395 396 397 398

        def tag_id_with_index(index)
          "#{@object_name}_#{index}_#{@method_name}"
        end
D
Initial  
David Heinemeier Hansson 已提交
399
    end
400

401
    class FormBuilder #:nodoc:
402 403 404
      # The methods which wrap a form helper call.
      class_inheritable_accessor :field_helpers
      self.field_helpers = (FormHelper.instance_methods - ['form_for'])
405 406 407

      attr_accessor :object_name, :object

408 409
      def initialize(object_name, object, template, options, proc)
        @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc        
410
      end
411 412
      
      (field_helpers - %w(check_box radio_button)).each do |selector|
413 414 415 416 417 418 419 420 421 422 423 424 425
        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
      
      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 = {})
426
        @template.radio_button(@object_name, method, tag_value, options.merge(:object => @object))
427 428
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
429
  end
430
end