ehoch_simple_form 2.0.2.dev

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/CHANGELOG.md +257 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +797 -0
  4. data/lib/generators/simple_form/USAGE +3 -0
  5. data/lib/generators/simple_form/install_generator.rb +32 -0
  6. data/lib/generators/simple_form/templates/README +12 -0
  7. data/lib/generators/simple_form/templates/_form.html.erb +13 -0
  8. data/lib/generators/simple_form/templates/_form.html.haml +10 -0
  9. data/lib/generators/simple_form/templates/_form.html.slim +10 -0
  10. data/lib/generators/simple_form/templates/config/initializers/simple_form.rb.tt +181 -0
  11. data/lib/generators/simple_form/templates/config/locales/simple_form.en.yml +26 -0
  12. data/lib/simple_form.rb +215 -0
  13. data/lib/simple_form/action_view_extensions/builder.rb +338 -0
  14. data/lib/simple_form/action_view_extensions/form_helper.rb +74 -0
  15. data/lib/simple_form/components.rb +20 -0
  16. data/lib/simple_form/components/errors.rb +35 -0
  17. data/lib/simple_form/components/hints.rb +18 -0
  18. data/lib/simple_form/components/html5.rb +26 -0
  19. data/lib/simple_form/components/label_input.rb +15 -0
  20. data/lib/simple_form/components/labels.rb +79 -0
  21. data/lib/simple_form/components/maxlength.rb +41 -0
  22. data/lib/simple_form/components/min_max.rb +50 -0
  23. data/lib/simple_form/components/pattern.rb +34 -0
  24. data/lib/simple_form/components/placeholders.rb +16 -0
  25. data/lib/simple_form/components/readonly.rb +22 -0
  26. data/lib/simple_form/core_ext/hash.rb +16 -0
  27. data/lib/simple_form/error_notification.rb +48 -0
  28. data/lib/simple_form/form_builder.rb +472 -0
  29. data/lib/simple_form/helpers.rb +12 -0
  30. data/lib/simple_form/helpers/autofocus.rb +11 -0
  31. data/lib/simple_form/helpers/disabled.rb +15 -0
  32. data/lib/simple_form/helpers/readonly.rb +15 -0
  33. data/lib/simple_form/helpers/required.rb +35 -0
  34. data/lib/simple_form/helpers/validators.rb +44 -0
  35. data/lib/simple_form/i18n_cache.rb +22 -0
  36. data/lib/simple_form/inputs.rb +21 -0
  37. data/lib/simple_form/inputs/base.rb +162 -0
  38. data/lib/simple_form/inputs/block_input.rb +14 -0
  39. data/lib/simple_form/inputs/boolean_input.rb +64 -0
  40. data/lib/simple_form/inputs/collection_check_boxes_input.rb +21 -0
  41. data/lib/simple_form/inputs/collection_input.rb +101 -0
  42. data/lib/simple_form/inputs/collection_radio_buttons_input.rb +63 -0
  43. data/lib/simple_form/inputs/collection_select_input.rb +14 -0
  44. data/lib/simple_form/inputs/date_time_input.rb +28 -0
  45. data/lib/simple_form/inputs/file_input.rb +9 -0
  46. data/lib/simple_form/inputs/grouped_collection_select_input.rb +41 -0
  47. data/lib/simple_form/inputs/hidden_input.rb +17 -0
  48. data/lib/simple_form/inputs/numeric_input.rb +24 -0
  49. data/lib/simple_form/inputs/password_input.rb +12 -0
  50. data/lib/simple_form/inputs/priority_input.rb +24 -0
  51. data/lib/simple_form/inputs/range_input.rb +14 -0
  52. data/lib/simple_form/inputs/string_input.rb +23 -0
  53. data/lib/simple_form/inputs/text_input.rb +11 -0
  54. data/lib/simple_form/map_type.rb +16 -0
  55. data/lib/simple_form/version.rb +3 -0
  56. data/lib/simple_form/wrappers.rb +8 -0
  57. data/lib/simple_form/wrappers/builder.rb +103 -0
  58. data/lib/simple_form/wrappers/many.rb +69 -0
  59. data/lib/simple_form/wrappers/root.rb +34 -0
  60. data/lib/simple_form/wrappers/single.rb +18 -0
  61. data/test/action_view_extensions/builder_test.rb +577 -0
  62. data/test/action_view_extensions/form_helper_test.rb +104 -0
  63. data/test/components/label_test.rb +310 -0
  64. data/test/form_builder/association_test.rb +177 -0
  65. data/test/form_builder/button_test.rb +47 -0
  66. data/test/form_builder/error_notification_test.rb +79 -0
  67. data/test/form_builder/error_test.rb +121 -0
  68. data/test/form_builder/general_test.rb +356 -0
  69. data/test/form_builder/hint_test.rb +139 -0
  70. data/test/form_builder/input_field_test.rb +63 -0
  71. data/test/form_builder/label_test.rb +71 -0
  72. data/test/form_builder/wrapper_test.rb +149 -0
  73. data/test/generators/simple_form_generator_test.rb +32 -0
  74. data/test/inputs/boolean_input_test.rb +108 -0
  75. data/test/inputs/collection_check_boxes_input_test.rb +224 -0
  76. data/test/inputs/collection_radio_buttons_input_test.rb +326 -0
  77. data/test/inputs/collection_select_input_test.rb +241 -0
  78. data/test/inputs/datetime_input_test.rb +99 -0
  79. data/test/inputs/disabled_test.rb +38 -0
  80. data/test/inputs/discovery_test.rb +61 -0
  81. data/test/inputs/file_input_test.rb +16 -0
  82. data/test/inputs/general_test.rb +69 -0
  83. data/test/inputs/grouped_collection_select_input_test.rb +118 -0
  84. data/test/inputs/hidden_input_test.rb +30 -0
  85. data/test/inputs/numeric_input_test.rb +173 -0
  86. data/test/inputs/priority_input_test.rb +43 -0
  87. data/test/inputs/readonly_test.rb +61 -0
  88. data/test/inputs/required_test.rb +113 -0
  89. data/test/inputs/string_input_test.rb +140 -0
  90. data/test/inputs/text_input_test.rb +24 -0
  91. data/test/simple_form_test.rb +9 -0
  92. data/test/support/discovery_inputs.rb +21 -0
  93. data/test/support/misc_helpers.rb +102 -0
  94. data/test/support/mock_controller.rb +24 -0
  95. data/test/support/models.rb +210 -0
  96. data/test/test_helper.rb +90 -0
  97. metadata +210 -0
@@ -0,0 +1,18 @@
1
+ module SimpleForm
2
+ module Components
3
+ # Needs to be enabled in order to do automatic lookups.
4
+ module Hints
5
+ def hint
6
+ @hint ||= begin
7
+ hint = options[:hint]
8
+ hint_content = hint.is_a?(String) ? hint : translate(:hints)
9
+ hint_content.html_safe if hint_content
10
+ end
11
+ end
12
+
13
+ def has_hint?
14
+ hint.present?
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ module SimpleForm
2
+ module Components
3
+ module HTML5
4
+ def initialize(*)
5
+ @html5 = false
6
+ end
7
+
8
+ def html5
9
+ @html5 = true
10
+ input_html_options[:required] = true if has_required?
11
+ nil
12
+ end
13
+
14
+ def html5?
15
+ @html5
16
+ end
17
+
18
+ def has_required?
19
+ # We need to check browser_validations because
20
+ # some browsers are still checking required even
21
+ # if novalidate was given.
22
+ required_field? && SimpleForm.browser_validations
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ module SimpleForm
2
+ module Components
3
+ module LabelInput
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include SimpleForm::Components::Labels
8
+ end
9
+
10
+ def label_input
11
+ (options[:label] == false ? "" : label) + input
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ module SimpleForm
2
+ module Components
3
+ module Labels
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods #:nodoc:
7
+ def translate_required_html
8
+ i18n_cache :translate_required_html do
9
+ I18n.t(:"simple_form.required.html", :default =>
10
+ %[<abbr title="#{translate_required_text}">#{translate_required_mark}</abbr>]
11
+ )
12
+ end
13
+ end
14
+
15
+ def translate_required_text
16
+ I18n.t(:"simple_form.required.text", :default => 'required')
17
+ end
18
+
19
+ def translate_required_mark
20
+ I18n.t(:"simple_form.required.mark", :default => '*')
21
+ end
22
+ end
23
+
24
+ def label
25
+ if generate_label_for_attribute?
26
+ @builder.label(label_target, label_text, label_html_options)
27
+ else
28
+ template.label_tag(nil, label_text, label_html_options)
29
+ end
30
+ end
31
+
32
+ def label_text
33
+ SimpleForm.label_text.call(raw_label_text, required_label_text).strip.html_safe
34
+ end
35
+
36
+ def label_target
37
+ attribute_name
38
+ end
39
+
40
+ def label_html_options
41
+ label_html_classes = SimpleForm.additional_classes_for(:label) {
42
+ [input_type, required_class, SimpleForm.label_class].compact
43
+ }
44
+
45
+ label_options = html_options_for(:label, label_html_classes)
46
+ if options.key?(:input_html) && options[:input_html].key?(:id)
47
+ label_options[:for] = options[:input_html][:id]
48
+ end
49
+ label_options
50
+ end
51
+
52
+ protected
53
+
54
+ def raw_label_text #:nodoc:
55
+ options[:label] || label_translation
56
+ end
57
+
58
+ # Default required text when attribute is required.
59
+ def required_label_text #:nodoc:
60
+ required_field? ? self.class.translate_required_html.dup : ''
61
+ end
62
+
63
+ # First check labels translation and then human attribute name.
64
+ def label_translation #:nodoc:
65
+ if SimpleForm.translate_labels && (translated_label = translate(:labels))
66
+ translated_label
67
+ elsif object.class.respond_to?(:human_attribute_name)
68
+ object.class.human_attribute_name(reflection_or_attribute_name.to_s)
69
+ else
70
+ attribute_name.to_s.humanize
71
+ end
72
+ end
73
+
74
+ def generate_label_for_attribute?
75
+ true
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,41 @@
1
+ module SimpleForm
2
+ module Components
3
+ # Needs to be enabled in order to do automatic lookups.
4
+ module Maxlength
5
+ def maxlength
6
+ input_html_options[:maxlength] ||= maximum_length_from_validation || limit
7
+ nil
8
+ end
9
+
10
+ private
11
+
12
+ def maximum_length_from_validation
13
+ maxlength = options[:maxlength]
14
+ if maxlength.is_a?(String) || maxlength.is_a?(Integer)
15
+ maxlength
16
+ else
17
+ length_validator = find_length_validator
18
+
19
+ if length_validator && !has_tokenizer?(length_validator)
20
+ length_validator.options[:is] || length_validator.options[:maximum]
21
+ end
22
+ end
23
+ end
24
+
25
+ def find_length_validator
26
+ find_validator(ActiveModel::Validations::LengthValidator)
27
+ end
28
+
29
+ def has_tokenizer?(length_validator)
30
+ tokenizer = length_validator.options[:tokenizer]
31
+
32
+ # TODO: Remove this check when we drop Rails 3.0 support
33
+ if ActiveModel::Validations::LengthValidator.const_defined?(:DEFAULT_TOKENIZER)
34
+ tokenizer && tokenizer != ActiveModel::Validations::LengthValidator::DEFAULT_TOKENIZER
35
+ else
36
+ tokenizer
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,50 @@
1
+ module SimpleForm
2
+ module Components
3
+ module MinMax
4
+ def min_max
5
+ if numeric_validator = find_numericality_validator
6
+ validator_options = numeric_validator.options
7
+ input_html_options[:min] ||= minimum_value(validator_options)
8
+ input_html_options[:max] ||= maximum_value(validator_options)
9
+ end
10
+ nil
11
+ end
12
+
13
+ private
14
+
15
+ def integer?
16
+ input_type == :integer
17
+ end
18
+
19
+ def minimum_value(validator_options)
20
+ if integer? && validator_options.key?(:greater_than)
21
+ evaluate_numericality_validator_option(validator_options[:greater_than]) + 1
22
+ else
23
+ evaluate_numericality_validator_option(validator_options[:greater_than_or_equal_to])
24
+ end
25
+ end
26
+
27
+ def maximum_value(validator_options)
28
+ if integer? && validator_options.key?(:less_than)
29
+ evaluate_numericality_validator_option(validator_options[:less_than]) - 1
30
+ else
31
+ evaluate_numericality_validator_option(validator_options[:less_than_or_equal_to])
32
+ end
33
+ end
34
+
35
+ def find_numericality_validator
36
+ find_validator(ActiveModel::Validations::NumericalityValidator)
37
+ end
38
+
39
+ def evaluate_numericality_validator_option(option)
40
+ if option.is_a?(Numeric)
41
+ option
42
+ elsif option.is_a?(Symbol)
43
+ object.send(option)
44
+ elsif option.respond_to?(:call)
45
+ option.call(object)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ module SimpleForm
2
+ module Components
3
+ # Needs to be enabled in order to do automatic lookups.
4
+ module Pattern
5
+ def pattern
6
+ input_html_options[:pattern] ||= pattern_source
7
+ nil
8
+ end
9
+
10
+ private
11
+
12
+ def pattern_source
13
+ pattern = options[:pattern]
14
+ if pattern.is_a?(String)
15
+ pattern
16
+ elsif pattern_validator = find_pattern_validator
17
+ evaluate_format_validator_option(pattern_validator.options[:with]).source
18
+ end
19
+ end
20
+
21
+ def find_pattern_validator
22
+ find_validator(ActiveModel::Validations::FormatValidator)
23
+ end
24
+
25
+ def evaluate_format_validator_option(option)
26
+ if option.respond_to?(:call)
27
+ option.call(object)
28
+ else
29
+ option
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,16 @@
1
+ module SimpleForm
2
+ module Components
3
+ # Needs to be enabled in order to do automatic lookups.
4
+ module Placeholders
5
+ def placeholder
6
+ input_html_options[:placeholder] ||= placeholder_text
7
+ nil
8
+ end
9
+
10
+ def placeholder_text
11
+ placeholder = options[:placeholder]
12
+ placeholder.is_a?(String) ? placeholder : translate(:placeholders)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module SimpleForm
2
+ module Components
3
+ # Needs to be enabled in order to do automatic lookups.
4
+ module Readonly
5
+ def readonly
6
+ if readonly_attribute? && !has_readonly?
7
+ input_html_options[:readonly] ||= true
8
+ input_html_classes << :readonly
9
+ end
10
+ nil
11
+ end
12
+
13
+ private
14
+
15
+ def readonly_attribute?
16
+ object.class.respond_to?(:readonly_attributes) &&
17
+ object.persisted? &&
18
+ object.class.readonly_attributes.include?(attribute_name)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # TODO: Delete this file when we drop support for Rails 3.0
2
+ # This method is already implemented in active_support 3.1
3
+
4
+ unless Hash.new.respond_to?(:deep_dup)
5
+ class Hash
6
+ # Returns a deep copy of hash.
7
+ def deep_dup
8
+ duplicate = self.dup
9
+ duplicate.each_pair do |k,v|
10
+ tv = duplicate[k]
11
+ duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_dup : v
12
+ end
13
+ duplicate
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,48 @@
1
+ module SimpleForm
2
+ class ErrorNotification
3
+ delegate :object, :object_name, :template, :to => :@builder
4
+
5
+ def initialize(builder, options)
6
+ @builder = builder
7
+ @message = options.delete(:message)
8
+ @options = options
9
+ end
10
+
11
+ def render
12
+ if has_errors?
13
+ template.content_tag(error_notification_tag, error_message, html_options)
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ def errors
20
+ object.errors
21
+ end
22
+
23
+ def has_errors?
24
+ object && object.respond_to?(:errors) && errors.present?
25
+ end
26
+
27
+ def error_message
28
+ (@message || translate_error_notification).html_safe
29
+ end
30
+
31
+ def error_notification_tag
32
+ SimpleForm.error_notification_tag
33
+ end
34
+
35
+ def html_options
36
+ @options[:class] = "#{SimpleForm.error_notification_class} #{@options[:class]}".strip
37
+ @options
38
+ end
39
+
40
+ def translate_error_notification
41
+ lookups = []
42
+ lookups << :"#{object_name}"
43
+ lookups << :default_message
44
+ lookups << "Some errors were found, please take a look:"
45
+ I18n.t(lookups.shift, :scope => :"simple_form.error_notification", :default => lookups)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,472 @@
1
+ require 'simple_form/core_ext/hash'
2
+
3
+ module SimpleForm
4
+ class FormBuilder < ActionView::Helpers::FormBuilder
5
+ attr_reader :template, :object_name, :object, :wrapper
6
+
7
+ # When action is create or update, we still should use new and edit
8
+ ACTIONS = {
9
+ :create => :new,
10
+ :update => :edit
11
+ }
12
+
13
+ extend MapType
14
+ include SimpleForm::Inputs
15
+
16
+ map_type :text, :to => SimpleForm::Inputs::TextInput
17
+ map_type :file, :to => SimpleForm::Inputs::FileInput
18
+ map_type :string, :email, :search, :tel, :url, :to => SimpleForm::Inputs::StringInput
19
+ map_type :password, :to => SimpleForm::Inputs::PasswordInput
20
+ map_type :integer, :decimal, :float, :to => SimpleForm::Inputs::NumericInput
21
+ map_type :range, :to => SimpleForm::Inputs::RangeInput
22
+ map_type :check_boxes, :to => SimpleForm::Inputs::CollectionCheckBoxesInput
23
+ map_type :radio_buttons, :to => SimpleForm::Inputs::CollectionRadioButtonsInput
24
+ map_type :select, :to => SimpleForm::Inputs::CollectionSelectInput
25
+ map_type :grouped_select, :to => SimpleForm::Inputs::GroupedCollectionSelectInput
26
+ map_type :date, :time, :datetime, :to => SimpleForm::Inputs::DateTimeInput
27
+ map_type :country, :time_zone, :to => SimpleForm::Inputs::PriorityInput
28
+ map_type :boolean, :to => SimpleForm::Inputs::BooleanInput
29
+
30
+ def self.discovery_cache
31
+ @discovery_cache ||= {}
32
+ end
33
+
34
+ def initialize(*) #:nodoc:
35
+ super
36
+ @defaults = options[:defaults]
37
+ @wrapper = SimpleForm.wrapper(options[:wrapper] || SimpleForm.default_wrapper)
38
+ end
39
+
40
+ # Basic input helper, combines all components in the stack to generate
41
+ # input html based on options the user define and some guesses through
42
+ # database column information. By default a call to input will generate
43
+ # label + input + hint (when defined) + errors (when exists), and all can
44
+ # be configured inside a wrapper html.
45
+ #
46
+ # == Examples
47
+ #
48
+ # # Imagine @user has error "can't be blank" on name
49
+ # simple_form_for @user do |f|
50
+ # f.input :name, :hint => 'My hint'
51
+ # end
52
+ #
53
+ # This is the output html (only the input portion, not the form):
54
+ #
55
+ # <label class="string required" for="user_name">
56
+ # <abbr title="required">*</abbr> Super User Name!
57
+ # </label>
58
+ # <input class="string required" id="user_name" maxlength="100"
59
+ # name="user[name]" size="100" type="text" value="Carlos" />
60
+ # <span class="hint">My hint</span>
61
+ # <span class="error">can't be blank</span>
62
+ #
63
+ # Each database type will render a default input, based on some mappings and
64
+ # heuristic to determine which is the best option.
65
+ #
66
+ # You have some options for the input to enable/disable some functions:
67
+ #
68
+ # :as => allows you to define the input type you want, for instance you
69
+ # can use it to generate a text field for a date column.
70
+ #
71
+ # :required => defines whether this attribute is required or not. True
72
+ # by default.
73
+ #
74
+ # The fact SimpleForm is built in components allow the interface to be unified.
75
+ # So, for instance, if you need to disable :hint for a given input, you can pass
76
+ # :hint => false. The same works for :error, :label and :wrapper.
77
+ #
78
+ # Besides the html for any component can be changed. So, if you want to change
79
+ # the label html you just need to give a hash to :label_html. To configure the
80
+ # input html, supply :input_html instead and so on.
81
+ #
82
+ # == Options
83
+ #
84
+ # Some inputs, as datetime, time and select allow you to give extra options, like
85
+ # prompt and/or include blank. Such options are given in plainly:
86
+ #
87
+ # f.input :created_at, :include_blank => true
88
+ #
89
+ # == Collection
90
+ #
91
+ # When playing with collections (:radio_buttons, :check_boxes and :select
92
+ # inputs), you have three extra options:
93
+ #
94
+ # :collection => use to determine the collection to generate the radio or select
95
+ #
96
+ # :label_method => the method to apply on the array collection to get the label
97
+ #
98
+ # :value_method => the method to apply on the array collection to get the value
99
+ #
100
+ # == Priority
101
+ #
102
+ # Some inputs, as :time_zone and :country accepts a :priority option. If none is
103
+ # given SimpleForm.time_zone_priority and SimpleForm.country_priority are used respectivelly.
104
+ #
105
+ def input(attribute_name, options={}, &block)
106
+ options = @defaults.deep_dup.deep_merge(options) if @defaults
107
+
108
+ chosen =
109
+ if name = options[:wrapper]
110
+ name.respond_to?(:render) ? name : SimpleForm.wrapper(name)
111
+ else
112
+ wrapper
113
+ end
114
+
115
+ chosen.render find_input(attribute_name, options, &block)
116
+ end
117
+ alias :attribute :input
118
+
119
+ # Creates a input tag for the given attribute. All the given options
120
+ # are sent as :input_html.
121
+ #
122
+ # == Examples
123
+ #
124
+ # simple_form_for @user do |f|
125
+ # f.input_field :name
126
+ # end
127
+ #
128
+ # This is the output html (only the input portion, not the form):
129
+ #
130
+ # <input class="string required" id="user_name" maxlength="100"
131
+ # name="user[name]" size="100" type="text" value="Carlos" />
132
+ #
133
+ def input_field(attribute_name, options={})
134
+ options = options.dup
135
+ options[:input_html] = options.except(:as, :collection, :label_method, :value_method)
136
+ SimpleForm::Wrappers::Root.new([:input], :wrapper => false).render find_input(attribute_name, options)
137
+ end
138
+
139
+ # Helper for dealing with association selects/radios, generating the
140
+ # collection automatically. It's just a wrapper to input, so all options
141
+ # supported in input are also supported by association. Some extra options
142
+ # can also be given:
143
+ #
144
+ # == Examples
145
+ #
146
+ # simple_form_for @user do |f|
147
+ # f.association :company # Company.all
148
+ # end
149
+ #
150
+ # f.association :company, :collection => Company.all(:order => 'name')
151
+ # # Same as using :order option, but overriding collection
152
+ #
153
+ # == Block
154
+ #
155
+ # When a block is given, association simple behaves as a proxy to
156
+ # simple_fields_for:
157
+ #
158
+ # f.association :company do |c|
159
+ # c.input :name
160
+ # c.input :type
161
+ # end
162
+ #
163
+ # From the options above, only :collection can also be supplied.
164
+ #
165
+ def association(association, options={}, &block)
166
+ options = options.dup
167
+
168
+ return simple_fields_for(*[association,
169
+ options.delete(:collection), options].compact, &block) if block_given?
170
+
171
+ raise ArgumentError, "Association cannot be used in forms not associated with an object" unless @object
172
+
173
+ reflection = find_association_reflection(association)
174
+ raise "Association #{association.inspect} not found" unless reflection
175
+
176
+ options[:as] ||= :select
177
+ options[:collection] ||= reflection.klass.all(reflection.options.slice(:conditions, :order))
178
+
179
+ attribute = case reflection.macro
180
+ when :belongs_to
181
+ (reflection.respond_to?(:options) && reflection.options[:foreign_key]) || :"#{reflection.name}_id"
182
+ when :has_one
183
+ raise ":has_one associations are not supported by f.association"
184
+ else
185
+ if options[:as] == :select
186
+ html_options = options[:input_html] ||= {}
187
+ html_options[:size] ||= 5
188
+ html_options[:multiple] = true unless html_options.key?(:multiple)
189
+ end
190
+
191
+ # Force the association to be preloaded for performance.
192
+ if options[:preload] != false && object.respond_to?(association)
193
+ target = object.send(association)
194
+ target.to_a if target.respond_to?(:to_a)
195
+ end
196
+
197
+ :"#{reflection.name.to_s.singularize}_ids"
198
+ end
199
+
200
+ input(attribute, options.merge(:reflection => reflection))
201
+ end
202
+
203
+ # Creates a button:
204
+ #
205
+ # form_for @user do |f|
206
+ # f.button :submit
207
+ # end
208
+ #
209
+ # It just acts as a proxy to method name given. We also alias original Rails
210
+ # button implementation (3.2 forward (to delegate to the original when
211
+ # calling `f.button :button`.
212
+ #
213
+ # TODO: remove if condition when supporting only Rails 3.2 forward.
214
+ alias_method :button_button, :button if method_defined?(:button)
215
+ def button(type, *args, &block)
216
+ options = args.extract_options!.dup
217
+ options[:class] = [SimpleForm.button_class, options[:class]].compact
218
+ args << options
219
+ if respond_to?("#{type}_button")
220
+ send("#{type}_button", *args, &block)
221
+ else
222
+ send(type, *args, &block)
223
+ end
224
+ end
225
+
226
+ # Creates an error tag based on the given attribute, only when the attribute
227
+ # contains errors. All the given options are sent as :error_html.
228
+ #
229
+ # == Examples
230
+ #
231
+ # f.error :name
232
+ # f.error :name, :id => "cool_error"
233
+ #
234
+ def error(attribute_name, options={})
235
+ options = options.dup
236
+
237
+ options[:error_html] = options.except(:error_tag, :error_prefix, :error_method)
238
+ column = find_attribute_column(attribute_name)
239
+ input_type = default_input_type(attribute_name, column, options)
240
+ wrapper.find(:error).
241
+ render(SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options))
242
+ end
243
+
244
+ # Return the error but also considering its name. This is used
245
+ # when errors for a hidden field need to be shown.
246
+ #
247
+ # == Examples
248
+ #
249
+ # f.full_error :token #=> <span class="error">Token is invalid</span>
250
+ #
251
+ def full_error(attribute_name, options={})
252
+ options = options.dup
253
+
254
+ options[:error_prefix] ||= if object.class.respond_to?(:human_attribute_name)
255
+ object.class.human_attribute_name(attribute_name.to_s)
256
+ else
257
+ attribute_name.to_s.humanize
258
+ end
259
+
260
+ error(attribute_name, options)
261
+ end
262
+
263
+ # Creates a hint tag for the given attribute. Accepts a symbol indicating
264
+ # an attribute for I18n lookup or a string. All the given options are sent
265
+ # as :hint_html.
266
+ #
267
+ # == Examples
268
+ #
269
+ # f.hint :name # Do I18n lookup
270
+ # f.hint :name, :id => "cool_hint"
271
+ # f.hint "Don't forget to accept this"
272
+ #
273
+ def hint(attribute_name, options={})
274
+ options = options.dup
275
+
276
+ options[:hint_html] = options.except(:hint_tag, :hint)
277
+ if attribute_name.is_a?(String)
278
+ options[:hint] = attribute_name
279
+ attribute_name, column, input_type = nil, nil, nil
280
+ else
281
+ column = find_attribute_column(attribute_name)
282
+ input_type = default_input_type(attribute_name, column, options)
283
+ end
284
+
285
+ wrapper.find(:hint).
286
+ render(SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options))
287
+ end
288
+
289
+ # Creates a default label tag for the given attribute. You can give a label
290
+ # through the :label option or using i18n. All the given options are sent
291
+ # as :label_html.
292
+ #
293
+ # == Examples
294
+ #
295
+ # f.label :name # Do I18n lookup
296
+ # f.label :name, "Name" # Same behavior as Rails, do not add required tag
297
+ # f.label :name, :label => "Name" # Same as above, but adds required tag
298
+ #
299
+ # f.label :name, :required => false
300
+ # f.label :name, :id => "cool_label"
301
+ #
302
+ def label(attribute_name, *args)
303
+ return super if args.first.is_a?(String) || block_given?
304
+
305
+ options = args.extract_options!.dup
306
+ options[:label_html] = options.except(:label, :required, :as)
307
+
308
+ column = find_attribute_column(attribute_name)
309
+ input_type = default_input_type(attribute_name, column, options)
310
+ SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options).label
311
+ end
312
+
313
+ # Creates an error notification message that only appears when the form object
314
+ # has some error. You can give a specific message with the :message option,
315
+ # otherwise it will look for a message using I18n. All other options given are
316
+ # passed straight as html options to the html tag.
317
+ #
318
+ # == Examples
319
+ #
320
+ # f.error_notification
321
+ # f.error_notification :message => 'Something went wrong'
322
+ # f.error_notification :id => 'user_error_message', :class => 'form_error'
323
+ #
324
+ def error_notification(options={})
325
+ SimpleForm::ErrorNotification.new(self, options).render
326
+ end
327
+
328
+ # Extract the model names from the object_name mess, ignoring numeric and
329
+ # explicit child indexes.
330
+ #
331
+ # Example:
332
+ #
333
+ # route[blocks_attributes][0][blocks_learning_object_attributes][1][foo_attributes]
334
+ # ["route", "blocks", "blocks_learning_object", "foo"]
335
+ #
336
+ def lookup_model_names
337
+ @lookup_model_names ||= begin
338
+ child_index = options[:child_index]
339
+ names = object_name.to_s.scan(/([a-zA-Z_]+)/).flatten
340
+ names.delete(child_index) if child_index
341
+ names.each { |name| name.gsub!('_attributes', '') }
342
+ names.freeze
343
+ end
344
+ end
345
+
346
+ # The action to be used in lookup.
347
+ def lookup_action
348
+ @lookup_action ||= begin
349
+ action = template.controller.action_name
350
+ return unless action
351
+ action = action.to_sym
352
+ ACTIONS[action] || action
353
+ end
354
+ end
355
+
356
+ private
357
+
358
+ # Find an input based on the attribute name.
359
+ def find_input(attribute_name, options={}, &block) #:nodoc:
360
+ column = find_attribute_column(attribute_name)
361
+ input_type = default_input_type(attribute_name, column, options)
362
+
363
+ if input_type == :radio
364
+ SimpleForm.deprecation_warn "Using `:as => :radio` as input type is " \
365
+ "deprecated, please change it to `:as => :radio_buttons`."
366
+ input_type = :radio_buttons
367
+ end
368
+
369
+ if block_given?
370
+ SimpleForm::Inputs::BlockInput.new(self, attribute_name, column, input_type, options, &block)
371
+ else
372
+ find_mapping(input_type).new(self, attribute_name, column, input_type, options)
373
+ end
374
+ end
375
+
376
+ # Attempt to guess the better input type given the defined options. By
377
+ # default alwayls fallback to the user :as option, or to a :select when a
378
+ # collection is given.
379
+ def default_input_type(attribute_name, column, options) #:nodoc:
380
+ return options[:as].to_sym if options[:as]
381
+ return :select if options[:collection]
382
+ custom_type = find_custom_type(attribute_name.to_s) and return custom_type
383
+
384
+ input_type = column.try(:type)
385
+ case input_type
386
+ when :timestamp
387
+ :datetime
388
+ when :string, nil
389
+ case attribute_name.to_s
390
+ when /password/ then :password
391
+ when /time_zone/ then :time_zone
392
+ when /country/ then :country
393
+ when /email/ then :email
394
+ when /phone/ then :tel
395
+ when /url/ then :url
396
+ else
397
+ file_method?(attribute_name) ? :file : (input_type || :string)
398
+ end
399
+ else
400
+ input_type
401
+ end
402
+ end
403
+
404
+ def find_custom_type(attribute_name) #:nodoc:
405
+ SimpleForm.input_mappings.find { |match, type|
406
+ attribute_name =~ match
407
+ }.try(:last) if SimpleForm.input_mappings
408
+ end
409
+
410
+ def file_method?(attribute_name) #:nodoc:
411
+ file = @object.send(attribute_name) if @object.respond_to?(attribute_name)
412
+ file && SimpleForm.file_methods.any? { |m| file.respond_to?(m) }
413
+ end
414
+
415
+ def find_attribute_column(attribute_name) #:nodoc:
416
+ if @object.respond_to?(:column_for_attribute)
417
+ @object.column_for_attribute(attribute_name)
418
+ end
419
+ end
420
+
421
+ def find_association_reflection(association) #:nodoc:
422
+ if @object.class.respond_to?(:reflect_on_association)
423
+ @object.class.reflect_on_association(association)
424
+ end
425
+ end
426
+
427
+ # Attempts to find a mapping. It follows the following rules:
428
+ #
429
+ # 1) It tries to find a registered mapping, if succeeds:
430
+ # a) Try to find an alternative with the same name in the Object scope
431
+ # b) Or use the found mapping
432
+ # 2) If not, fallbacks to #{input_type}Input
433
+ # 3) If not, fallbacks to SimpleForm::Inputs::#{input_type}Input
434
+ def find_mapping(input_type) #:nodoc:
435
+ discovery_cache[input_type] ||=
436
+ if mapping = self.class.mappings[input_type]
437
+ mapping_override(mapping) || mapping
438
+ else
439
+ camelized = "#{input_type.to_s.camelize}Input"
440
+ attempt_mapping(camelized, Object) || attempt_mapping(camelized, self.class) ||
441
+ raise("No input found for #{input_type}")
442
+ end
443
+ end
444
+
445
+ # If cache_discovery is enabled, use the class level cache that persists
446
+ # between requests, otherwise use the instance one.
447
+ def discovery_cache #:nodoc:
448
+ if SimpleForm.cache_discovery
449
+ self.class.discovery_cache
450
+ else
451
+ @discovery_cache ||= {}
452
+ end
453
+ end
454
+
455
+ def mapping_override(klass) #:nodoc:
456
+ name = klass.name
457
+ if name =~ /^SimpleForm::Inputs/
458
+ attempt_mapping name.split("::").last, Object
459
+ end
460
+ end
461
+
462
+ def attempt_mapping(mapping, at) #:nodoc:
463
+ return if SimpleForm.inputs_discovery == false && at == Object
464
+
465
+ begin
466
+ at.const_get(mapping)
467
+ rescue NameError => e
468
+ e.message =~ /#{mapping}$/ ? nil : raise
469
+ end
470
+ end
471
+ end
472
+ end