ehoch_simple_form 2.0.2.dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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