aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_view/helpers/form_helper.rb
blob: 105f464fc8798dc6fca2e3b25024debfaaf6d6c7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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">
    #     Name:           
    #     <%= text_field "person", "name", "size" => 20 %>
    #
    #     Password:       
    #     <%= 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">
    #     Name:           
    #     <input type="text" id="person_name" name="person[name]" 
    #       size="20" value="<%= @person.name %>" />
    #
    #     Password:       
    #     <input type="password" id="person_password" name="person[password]" 
    #       size="20" maxsize="20" value="<%= @person.password %>" />
    #
    #     Single?:
    #     <input type="checkbox" id="person_single" name="person[single] value="1" />
    #
    #     Description:
    #     <textarea cols="20" rows="40" id="person_description" name="person[description]">
    #       <%= @person.description %>
    #     </textarea>
    #
    #     <input type="submit" value="Save">
    #   </form>    
    #
    # There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, 
    # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
    module FormHelper
      # 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}" />
      def text_field(object, method, options = {}) 
        InstanceTag.new(object, method, self).to_input_field_tag("text", options)
      end

      # Works just like text_field, but returns a input tag of the "password" type instead.
      def password_field(object, method, options = {})
        InstanceTag.new(object, method, self).to_input_field_tag("password", options)
      end

      # Works just like text_field, but returns a input tag of the "hidden" type instead.
      def hidden_field(object, method, options = {})
        InstanceTag.new(object, method, self).to_input_field_tag("hidden", options)
      end

      # 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>
      def text_area(object, method, options = {})
        InstanceTag.new(object, method, self).to_text_area_tag(options)
      end
      
      # 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+
      # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything. 
      # 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")
      #     <input type="checkbox" id="post_validate" name="post[validated] value="1" checked="checked" />
      #     <input name="post[validated]" type="hidden" value="0" />
      #
      # Example (call, result). Imagine that @puppy.gooddog returns no:
      #   check_box("puppy", "gooddog", {}, "yes", "no")
      #     <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog] value="yes" />
      #     <input name="puppy[gooddog]" type="hidden" value="no" />
      def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0")
        InstanceTag.new(object, method, self).to_check_box_tag(options, checked_value, unchecked_value)
      end

      # 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
      # hash with +options+. 
      # Example (call, result). Imagine that @post.category returns "rails":
      #   radio_button("post", "category", "rails")
      #   radio_button("post", "category", "java")
      #     <input type="radio" id="post_category" name="post[category] value="rails" checked="checked" />
      #     <input type="radio" id="post_category" name="post[category] value="java" />
      #     
      def radio_button(object, method, tag_value, options = {})
        InstanceTag.new(object, method, self).to_radio_button_tag(tag_value, options)
      end
    end

    class InstanceTag #:nodoc:
      include Helpers::TagHelper

      attr_reader :method_name, :object_name
      
      DEFAULT_FIELD_OPTIONS     = { "size" => 30 } unless const_defined?("DEFAULT_FIELD_OPTIONS")
      DEFAULT_TEXT_AREA_OPTIONS = { "wrap" => "virtual", "cols" => 40, "rows" => 20 } unless const_defined?("DEFAULT_TEXT_AREA_OPTIONS")

      def initialize(object_name, method_name, template_object, local_binding = nil)
        @object_name, @method_name = object_name, method_name
        @template_object, @local_binding = template_object, local_binding
      end
      
      def to_input_field_tag(field_type, options = {})
        html_options = DEFAULT_FIELD_OPTIONS.merge(options)
        html_options.merge!({ "size" => options["maxlength"]}) if options["maxlength"] && !options["size"]
        html_options.delete("size") if field_type == "hidden"
        html_options.merge!({ "type" =>  field_type})
        html_options.merge!({ "value" => value_before_type_cast }) unless options["value"]
        add_default_name_and_id(html_options)
        tag("input", html_options)
      end

      def to_radio_button_tag(tag_value, options={})
        html_options = DEFAULT_FIELD_OPTIONS.merge(options)
        html_options.merge!({ "checked" => "checked" }) if value == tag_value
        html_options.merge!({ "type" => "radio", "value"=> tag_value.to_s })
                       
        add_default_name_and_id(html_options)
        tag("input", html_options)
      end
      
      def to_text_area_tag(options = {})
        options = DEFAULT_TEXT_AREA_OPTIONS.merge(options)
        add_default_name_and_id(options)
        content_tag("textarea", html_escape(value_before_type_cast), options)
      end

      def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
        options.merge!({ "checked" => "checked" }) if !value.nil? && ((value.is_a?(TrueClass) || value.is_a?(FalseClass)) ? value : value.to_i > 0)
        options.merge!({ "type" => "checkbox", "value" => checked_value })
        add_default_name_and_id(options)
        tag("input", options) << tag("input", ({ "name" => options['name'], "type" => "hidden", "value" => unchecked_value }))
      end

      def to_date_tag()
        defaults = { "discard_type" => true }
        date     = value || Date.today
        options  = Proc.new { |position| defaults.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }

        html_day_select(date, options.call(3)) +
        html_month_select(date, options.call(2)) + 
        html_year_select(date, options.call(1))
      end

      def to_boolean_select_tag(options = {})
        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

      def object
        @template_object.instance_variable_get "@#{@object_name}"
      end

      def value
        object.send(@method_name) unless object.nil?
      end

      def value_before_type_cast
        unless object.nil?
          object.respond_to?(@method_name + "_before_type_cast") ? 
            object.send(@method_name + "_before_type_cast") :
            object.send(@method_name)
        end
      end

      private
        def add_default_name_and_id(options)
          options['name'] = tag_name unless options.has_key? "name"
          options['id'] = tag_id unless options.has_key? "id"
        end
				
        def tag_name
          "#{@object_name}[#{@method_name}]"
        end

        def tag_id
          "#{@object_name}_#{@method_name}"
        end
    end
  end
end