form_helper.rb 17.5 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 50 51 52 53 54 55
    # If the object name contains square brackets the id for the object will be inserted. Example:
    #
    #   <%= textfield "person[]", "name" %> 
    # 
    # ...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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
      # 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:
      #
      #   <% form_for :person => @person, :url => { :action => "update" } do |f| %>
      #     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 
      # <tt>:person => person</tt> and all subsequent field calls save <tt>:person</tt> and <tt>:object => person</tt>.
      #
      # 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:
      #
      #   <% form_for :person => @person, :url => { :action => "update" } do |f| %>
      #     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.
      def form_for(options, *parameters_for_url, &proc)
        keys = [ :url, :method, :multipart ]
        leftover_keys = (options.keys - keys)

        case leftover_keys.length
          when 0 then raise 'No object given!'
          when 1 then
            object_name = leftover_keys.first
            object = options[object_name]
          else
            raise "Too many options: #{options.inspect}"
        end
        
        url_for_options = options[:url]
        additional_options = options.reject { |k, v| ![ :method, :multipart ].include?(k) }
        
        concat(form_tag(url_for_options, additional_options, *parameters_for_url), proc.binding)
117
        fields_for({ object_name => object }, &proc)
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
        concat(end_form_tag, proc.binding)
      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:
      #
      #   <% form_for :person => @person, :url => { :action => "update" } do |person_form| %>
      #     First name: <%= person_form.text_field :first_name %>
      #     Last name : <%= person_form.text_field :last_name %>
      #     
      #     <% fields_for :permission => @person.permission do |permission_fields| %>
      #       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.
      def fields_for(object = {}, &proc)
        form_builder = FormBuilder.new(object.keys.first, object.values.first, self, proc)
        proc.call(form_builder)
      end

D
Initial  
David Heinemeier Hansson 已提交
140 141 142 143 144 145 146
      # 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}" />
147 148
      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 已提交
149 150
      end

151
      # Works just like text_field, but returns an input tag of the "password" type instead.
152 153
      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 已提交
154 155
      end

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

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

D
Initial  
David Heinemeier Hansson 已提交
166 167 168 169 170 171 172 173 174
      # 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>
175 176
      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 已提交
177
      end
178

D
Initial  
David Heinemeier Hansson 已提交
179 180 181 182
      # 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+
183
      # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything.
D
Initial  
David Heinemeier Hansson 已提交
184 185 186 187
      # 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")
188
      #     <input type="checkbox" id="post_validate" name="post[validated]" value="1" checked="checked" />
D
David Heinemeier Hansson 已提交
189
      #     <input name="post[validated]" type="hidden" value="0" />
D
Initial  
David Heinemeier Hansson 已提交
190 191 192
      #
      # Example (call, result). Imagine that @puppy.gooddog returns no:
      #   check_box("puppy", "gooddog", {}, "yes", "no")
193
      #     <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
D
David Heinemeier Hansson 已提交
194
      #     <input name="puppy[gooddog]" type="hidden" value="no" />
195 196
      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 已提交
197
      end
198 199 200 201

      # 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
202
      # hash with +options+.
203 204 205
      # Example (call, result). Imagine that @post.category returns "rails":
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
206 207
      #     <input type="radio" id="post_category" name="post[category]" value="rails" checked="checked" />
      #     <input type="radio" id="post_category" name="post[category]" value="java" />
208
      #
209 210
      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)
211
      end
D
Initial  
David Heinemeier Hansson 已提交
212 213 214 215 216 217
    end

    class InstanceTag #:nodoc:
      include Helpers::TagHelper

      attr_reader :method_name, :object_name
218 219

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

224
      def initialize(object_name, method_name, template_object, local_binding = nil, object = nil)
225
        @object_name, @method_name = object_name.to_s, method_name.to_s
D
Initial  
David Heinemeier Hansson 已提交
226
        @template_object, @local_binding = template_object, local_binding
227
        @object = object
228
        if @object_name.sub!(/\[\]$/,"")
229
          @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast
230
        end
D
Initial  
David Heinemeier Hansson 已提交
231
      end
232

D
Initial  
David Heinemeier Hansson 已提交
233
      def to_input_field_tag(field_type, options = {})
234
        options = options.stringify_keys
235 236
        options["size"] ||= options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"]
        options = DEFAULT_FIELD_OPTIONS.merge(options)
237 238 239 240 241 242 243 244 245 246
        if field_type == "hidden"
          options.delete("size")
        end
        options["type"] = field_type
        options["value"] ||= value_before_type_cast unless field_type == "file"
        add_default_name_and_id(options)
        tag("input", options)
      end

      def to_radio_button_tag(tag_value, options = {})
247
        options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
248 249
        options["type"]     = "radio"
        options["value"]    = tag_value
250
        options["checked"]  = "checked" if value.to_s == tag_value.to_s
251
        pretty_tag_value    = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
252 253 254
        options["id"]       = @auto_index ?             
          "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
          "#{@object_name}_#{@method_name}_#{pretty_tag_value}"
255 256 257 258
        add_default_name_and_id(options)
        tag("input", options)
      end

D
Initial  
David Heinemeier Hansson 已提交
259
      def to_text_area_tag(options = {})
260
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
D
Initial  
David Heinemeier Hansson 已提交
261
        add_default_name_and_id(options)
262
        content_tag("textarea", html_escape(value_before_type_cast), options)
D
Initial  
David Heinemeier Hansson 已提交
263 264 265
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
266 267 268 269 270 271 272 273 274 275
        options = options.stringify_keys
        options["type"]     = "checkbox"
        options["value"]    = checked_value
        checked = case value
          when TrueClass, FalseClass
            value
          when NilClass
            false
          when Integer
            value != 0
276 277
          when String
            value == checked_value
278 279 280
          else
            value.to_i != 0
          end
281
        if checked || options["checked"] == "checked"
282 283 284 285
          options["checked"] = "checked"
        else
          options.delete("checked")
        end
D
Initial  
David Heinemeier Hansson 已提交
286
        add_default_name_and_id(options)
287
        tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value)
D
Initial  
David Heinemeier Hansson 已提交
288 289 290
      end

      def to_date_tag()
291
        defaults = DEFAULT_DATE_OPTIONS.dup
D
Initial  
David Heinemeier Hansson 已提交
292
        date     = value || Date.today
293
        options  = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
D
Initial  
David Heinemeier Hansson 已提交
294
        html_day_select(date, options.call(3)) +
295
        html_month_select(date, options.call(2)) +
D
Initial  
David Heinemeier Hansson 已提交
296 297 298 299
        html_year_select(date, options.call(1))
      end

      def to_boolean_select_tag(options = {})
300
        options = options.stringify_keys
D
Initial  
David Heinemeier Hansson 已提交
301 302 303 304 305 306 307 308 309
        add_default_name_and_id(options)
        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
310 311 312 313 314
      
      def to_content_tag(tag_name, options = {})
        content_tag(tag_name, value, options)
      end
      
D
Initial  
David Heinemeier Hansson 已提交
315
      def object
316
        @object || @template_object.instance_variable_get("@#{@object_name}")
D
Initial  
David Heinemeier Hansson 已提交
317 318 319
      end

      def value
320 321 322
        unless object.nil?
          object.send(@method_name)
        end
D
Initial  
David Heinemeier Hansson 已提交
323 324
      end

325
      def value_before_type_cast
326
        unless object.nil?
327
          object.respond_to?(@method_name + "_before_type_cast") ?
328 329 330
            object.send(@method_name + "_before_type_cast") :
            object.send(@method_name)
        end
331 332
      end

D
Initial  
David Heinemeier Hansson 已提交
333 334
      private
        def add_default_name_and_id(options)
335 336 337
          if options.has_key?("index")
            options["name"] ||= tag_name_with_index(options["index"])
            options["id"]   ||= tag_id_with_index(options["index"])
338
            options.delete("index")
339
          elsif @auto_index
340 341
            options["name"] ||= tag_name_with_index(@auto_index)
            options["id"]   ||= tag_id_with_index(@auto_index)
342
          else
343 344
            options["name"] ||= tag_name
            options["id"]   ||= tag_id
345
          end
D
Initial  
David Heinemeier Hansson 已提交
346
        end
347

D
Initial  
David Heinemeier Hansson 已提交
348 349 350
        def tag_name
          "#{@object_name}[#{@method_name}]"
        end
351

352 353 354
        def tag_name_with_index(index)
          "#{@object_name}[#{index}][#{@method_name}]"
        end
D
Initial  
David Heinemeier Hansson 已提交
355 356 357 358

        def tag_id
          "#{@object_name}_#{@method_name}"
        end
359 360 361 362

        def tag_id_with_index(index)
          "#{@object_name}_#{index}_#{@method_name}"
        end
D
Initial  
David Heinemeier Hansson 已提交
363
    end
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

    class FormBuilder
      def initialize(object_name, object, template, proc)
        @object_name, @object, @template, @proc = object_name, object, template, proc        
      end

      (FormHelper.instance_methods - [ :check_box, :radio_button ]).each do |selector|
        next if selector == "form_for"

        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 = {})
        @template.check_box(@object_name, method, tag_value, options.merge(:object => @object))
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
389
  end
390
end