simple_form 0.5 → 1.0.0

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.

Potentially problematic release.


This version of simple_form might be problematic. Click here for more details.

Files changed (48) hide show
  1. data/README.rdoc +339 -6
  2. data/generators/simple_form_install/USAGE +3 -0
  3. data/generators/simple_form_install/simple_form_install_generator.rb +19 -0
  4. data/generators/simple_form_install/templates/simple_form.rb +38 -0
  5. data/init.rb +1 -0
  6. data/lib/simple_form.rb +57 -1
  7. data/lib/simple_form/action_view_extensions/builder.rb +122 -0
  8. data/lib/simple_form/action_view_extensions/form_helper.rb +33 -0
  9. data/lib/simple_form/action_view_extensions/instance_tag.rb +37 -0
  10. data/lib/simple_form/components.rb +8 -0
  11. data/lib/simple_form/components/errors.rb +35 -0
  12. data/lib/simple_form/components/hints.rb +21 -0
  13. data/lib/simple_form/components/labels.rb +68 -0
  14. data/lib/simple_form/components/wrapper.rb +21 -0
  15. data/lib/simple_form/form_builder.rb +332 -0
  16. data/lib/simple_form/i18n_cache.rb +22 -0
  17. data/lib/simple_form/inputs.rb +12 -0
  18. data/lib/simple_form/inputs/base.rb +107 -0
  19. data/lib/simple_form/inputs/block_input.rb +13 -0
  20. data/lib/simple_form/inputs/collection_input.rb +58 -0
  21. data/lib/simple_form/inputs/date_time_input.rb +18 -0
  22. data/lib/simple_form/inputs/hidden_input.rb +11 -0
  23. data/lib/simple_form/inputs/mapping_input.rb +23 -0
  24. data/lib/simple_form/inputs/priority_input.rb +20 -0
  25. data/lib/simple_form/inputs/text_field_input.rb +16 -0
  26. data/lib/simple_form/locale/en.yml +14 -0
  27. data/lib/simple_form/map_type.rb +13 -0
  28. data/lib/simple_form/version.rb +3 -0
  29. data/test/action_view_extensions/builder_test.rb +172 -0
  30. data/test/action_view_extensions/form_helper_test.rb +50 -0
  31. data/test/components/error_test.rb +45 -0
  32. data/test/components/hint_test.rb +78 -0
  33. data/test/components/label_test.rb +170 -0
  34. data/test/form_builder_test.rb +550 -0
  35. data/test/inputs_test.rb +337 -0
  36. data/test/simple_form_test.rb +9 -0
  37. data/test/support/country_select/init.rb +1 -0
  38. data/test/support/country_select/install.rb +2 -0
  39. data/test/support/country_select/lib/country_select.rb +84 -0
  40. data/test/support/country_select/uninstall.rb +1 -0
  41. data/test/support/misc_helpers.rb +29 -0
  42. data/test/support/mock_controller.rb +11 -0
  43. data/test/support/mock_response.rb +14 -0
  44. data/test/support/models.rb +100 -0
  45. data/test/test_helper.rb +60 -0
  46. metadata +50 -10
  47. data/CHANGELOG +0 -27
  48. data/Rakefile +0 -17
@@ -0,0 +1,22 @@
1
+ module SimpleForm
2
+ # A lot of configuration values are retrived from I18n,
3
+ # like boolean collection, required string. This module provides
4
+ # caching facility to speed up form construction.
5
+ module I18nCache
6
+ def i18n_cache(key)
7
+ get_i18n_cache(key)[I18n.locale] ||= yield.freeze
8
+ end
9
+
10
+ def get_i18n_cache(key)
11
+ if class_variable_defined?(:"@@#{key}")
12
+ class_variable_get(:"@@#{key}")
13
+ else
14
+ reset_i18n_cache(key)
15
+ end
16
+ end
17
+
18
+ def reset_i18n_cache(key)
19
+ class_variable_set(:"@@#{key}", {})
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ autoload :Base, 'simple_form/inputs/base'
4
+ autoload :BlockInput, 'simple_form/inputs/block_input'
5
+ autoload :CollectionInput, 'simple_form/inputs/collection_input'
6
+ autoload :DateTimeInput, 'simple_form/inputs/date_time_input'
7
+ autoload :HiddenInput, 'simple_form/inputs/hidden_input'
8
+ autoload :MappingInput, 'simple_form/inputs/mapping_input'
9
+ autoload :PriorityInput, 'simple_form/inputs/priority_input'
10
+ autoload :TextFieldInput, 'simple_form/inputs/text_field_input'
11
+ end
12
+ end
@@ -0,0 +1,107 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class Base
4
+ extend I18nCache
5
+
6
+ # When action is create or update, we still should use new and edit
7
+ ACTIONS = {
8
+ :create => :new,
9
+ :update => :edit
10
+ }
11
+
12
+ include SimpleForm::Components::Errors
13
+ include SimpleForm::Components::Hints
14
+ include SimpleForm::Components::Labels
15
+ include SimpleForm::Components::Wrapper
16
+
17
+ delegate :template, :object, :object_name, :attribute_name, :column,
18
+ :reflection, :input_type, :options, :to => :@builder
19
+
20
+ def initialize(builder)
21
+ @builder = builder
22
+ end
23
+
24
+ def input
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def input_options
29
+ options
30
+ end
31
+
32
+ def input_html_options
33
+ html_options_for(:input, input_type, required_class)
34
+ end
35
+
36
+ def render
37
+ content = SimpleForm.components.map do |component|
38
+ next if options[component] == false
39
+ send(component)
40
+ end
41
+ content.compact!
42
+ wrap(content.join).html_safe!
43
+ end
44
+
45
+ protected
46
+
47
+ def attribute_required?
48
+ options[:required] != false
49
+ end
50
+
51
+ def required_class
52
+ attribute_required? ? :required : :optional
53
+ end
54
+
55
+ # Find reflection name when available, otherwise use attribute
56
+ def reflection_or_attribute_name
57
+ reflection ? reflection.name : attribute_name
58
+ end
59
+
60
+ # Retrieve options for the given namespace from the options hash
61
+ def html_options_for(namespace, *extra)
62
+ html_options = options[:"#{namespace}_html"] || {}
63
+ html_options[:class] = (extra << html_options[:class]).join(' ').strip if extra.present?
64
+ html_options
65
+ end
66
+
67
+ # Lookup translations for the given namespace using I18n, based on object name,
68
+ # actual action and attribute name. Lookup priority as follows:
69
+ #
70
+ # simple_form.{namespace}.{model}.{action}.{attribute}
71
+ # simple_form.{namespace}.{model}.{attribute}
72
+ # simple_form.{namespace}.{attribute}
73
+ #
74
+ # Namespace is used for :labels and :hints.
75
+ #
76
+ # Model is the actual object name, for a @user object you'll have :user.
77
+ # Action is the action being rendered, usually :new or :edit.
78
+ # And attribute is the attribute itself, :name for example.
79
+ #
80
+ # Example:
81
+ #
82
+ # simple_form:
83
+ # labels:
84
+ # user:
85
+ # new:
86
+ # email: 'E-mail para efetuar o sign in.'
87
+ # edit:
88
+ # email: 'E-mail.'
89
+ #
90
+ # Take a look at our locale example file.
91
+ def translate(namespace, default='')
92
+ lookups = []
93
+ lookups << :"#{object_name}.#{lookup_action}.#{reflection_or_attribute_name}"
94
+ lookups << :"#{object_name}.#{reflection_or_attribute_name}"
95
+ lookups << :"#{reflection_or_attribute_name}"
96
+ lookups << default
97
+ I18n.t(lookups.shift, :scope => :"simple_form.#{namespace}", :default => lookups)
98
+ end
99
+
100
+ # The action to be used in lookup.
101
+ def lookup_action
102
+ action = template.controller.action_name.to_sym
103
+ ACTIONS[action] || action
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,13 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class BlockInput < Base
4
+ def initialize(builder, block)
5
+ @builder, @block = builder, block
6
+ end
7
+
8
+ def input
9
+ template.capture(&@block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class CollectionInput < Base
4
+ # Default boolean collection for use with selects/radios when no
5
+ # collection is given. Always fallback to this boolean collection.
6
+ # Texts can be translated using i18n in "simple_form.yes" and
7
+ # "simple_form.no" keys. See the example locale file.
8
+ def self.boolean_collection
9
+ i18n_cache :boolean_collection do
10
+ [ [I18n.t(:"simple_form.yes", :default => 'Yes'), true],
11
+ [I18n.t(:"simple_form.no", :default => 'No'), false] ]
12
+ end
13
+ end
14
+
15
+ def input
16
+ collection = (options[:collection] || self.class.boolean_collection).to_a
17
+ detect_collection_methods(collection, options)
18
+ @builder.send(:"collection_#{input_type}", attribute_name, collection, options[:value_method],
19
+ options[:label_method], input_options, input_html_options)
20
+ end
21
+
22
+ def input_options
23
+ options = super
24
+ options[:include_blank] = true unless skip_include_blank?
25
+ options
26
+ end
27
+
28
+ protected
29
+
30
+ # Check if :include_blank must be included by default.
31
+ def skip_include_blank?
32
+ (options.keys & [:prompt, :include_blank, :default, :selected]).any? ||
33
+ options[:input_html].try(:[], :multiple)
34
+ end
35
+
36
+ # Detect the right method to find the label and value for a collection.
37
+ # If no label or value method are defined, will attempt to find them based
38
+ # on default label and value methods that can be configured through
39
+ # SimpleForm.collection_label_methods and
40
+ # SimpleForm.collection_value_methods.
41
+ def detect_collection_methods(collection, options)
42
+ sample = collection.first || collection.last
43
+
44
+ case sample
45
+ when Array
46
+ label, value = :first, :last
47
+ when Integer
48
+ label, value = :to_s, :to_i
49
+ when String, NilClass
50
+ label, value = :to_s, :to_s
51
+ end
52
+
53
+ options[:label_method] ||= label || SimpleForm.collection_label_methods.find { |m| sample.respond_to?(m) }
54
+ options[:value_method] ||= value || SimpleForm.collection_value_methods.find { |m| sample.respond_to?(m) }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class DateTimeInput < Base
4
+ def input
5
+ @builder.send(:"#{input_type}_select", attribute_name, input_options, input_html_options)
6
+ end
7
+
8
+ def label_target
9
+ case input_type
10
+ when :date, :datetime
11
+ "#{attribute_name}_1i"
12
+ when :time
13
+ "#{attribute_name}_4i"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ # Handles hidden input.
4
+ class HiddenInput < Base
5
+ def render
6
+ @builder.hidden_field(attribute_name, input_html_options)
7
+ end
8
+ alias :input :render
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ # Uses MapType to handle basic input types.
4
+ class MappingInput < Base
5
+ extend MapType
6
+
7
+ map_type :boolean, :to => :check_box
8
+ map_type :password, :to => :password_field
9
+ map_type :text, :to => :text_area
10
+ map_type :file, :to => :file_field
11
+
12
+ def input
13
+ @builder.send(input_method, attribute_name, input_html_options)
14
+ end
15
+
16
+ def input_method
17
+ method = self.class.mappings[input_type]
18
+ raise "Could not find method for #{input_type.inspect}" unless method
19
+ method
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class PriorityInput < CollectionInput
4
+ def input
5
+ @builder.send(:"#{input_type}_select", attribute_name, input_priority,
6
+ input_options, input_html_options)
7
+ end
8
+
9
+ def input_priority
10
+ options[:priority] || SimpleForm.send(:"#{input_type}_priority")
11
+ end
12
+
13
+ protected
14
+
15
+ def skip_include_blank?
16
+ super || input_priority.present?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ # Handles common text field inputs, as String, Numeric, Float and Decimal.
4
+ class TextFieldInput < Base
5
+ def input
6
+ @builder.text_field(attribute_name, input_html_options)
7
+ end
8
+
9
+ def input_html_options
10
+ input_options = super
11
+ input_options[:maxlength] ||= column.limit if column
12
+ input_options
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ en:
2
+ simple_form:
3
+ "yes": 'Yes'
4
+ "no": 'No'
5
+ buttons:
6
+ create: 'Create {{model}}'
7
+ update: 'Update {{model}}'
8
+ submit: 'Submit {{model}}'
9
+ required:
10
+ text: 'required'
11
+ mark: '*'
12
+ # You can uncomment the line below if you need to overwrite the whole required html.
13
+ # When using html, text and mark won't be used.
14
+ # html: '<abbr title="required">*</abbr>'
@@ -0,0 +1,13 @@
1
+ module SimpleForm
2
+ module MapType
3
+ def mappings
4
+ @mappings ||= {}
5
+ end
6
+
7
+ def map_type(*types)
8
+ options = types.extract_options!
9
+ raise ArgumentError, "You need to give :to as option to map_type" unless options[:to]
10
+ types.each { |t| mappings[t] = options[:to] }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleForm
2
+ VERSION = "1.0.0".freeze
3
+ end
@@ -0,0 +1,172 @@
1
+ require 'test_helper'
2
+
3
+ class BuilderTest < ActionView::TestCase
4
+ # COLLECTION RADIO
5
+ test 'collection radio accepts a collection and generate inputs from value method' do
6
+ form_for @user do |f|
7
+ concat f.collection_radio :active, [true, false], :to_s, :to_s
8
+ end
9
+
10
+ assert_select 'form input[type=radio][value=true]#user_active_true'
11
+ assert_select 'form input[type=radio][value=false]#user_active_false'
12
+ end
13
+
14
+ test 'collection radio accepts a collection and generate inputs from label method' do
15
+ form_for @user do |f|
16
+ concat f.collection_radio :active, [true, false], :to_s, :to_s
17
+ end
18
+
19
+ assert_select 'form label.collection_radio[for=user_active_true]', 'true'
20
+ assert_select 'form label.collection_radio[for=user_active_false]', 'false'
21
+ end
22
+
23
+ test 'collection radio accepts checked item' do
24
+ form_for @user do |f|
25
+ concat f.collection_radio :active, [[1, true], [0, false]], :last, :first, :checked => true
26
+ end
27
+
28
+ assert_select 'form input[type=radio][value=true][checked=checked]'
29
+ assert_no_select 'form input[type=radio][value=false][checked=checked]'
30
+ end
31
+
32
+ test 'collection radio accepts multiple disabled items' do
33
+ collection = [[1, true], [0, false], [2, 'other']]
34
+ form_for @user do |f|
35
+ concat f.collection_radio :active, collection, :last, :first, :disabled => [true, false]
36
+ end
37
+
38
+ assert_select 'form input[type=radio][value=true][disabled=disabled]'
39
+ assert_select 'form input[type=radio][value=false][disabled=disabled]'
40
+ assert_no_select 'form input[type=radio][value=other][disabled=disabled]'
41
+ end
42
+
43
+ test 'collection radio accepts single disable item' do
44
+ collection = [[1, true], [0, false]]
45
+ form_for @user do |f|
46
+ concat f.collection_radio :active, collection, :last, :first, :disabled => true
47
+ end
48
+
49
+ assert_select 'form input[type=radio][value=true][disabled=disabled]'
50
+ assert_no_select 'form input[type=radio][value=false][disabled=disabled]'
51
+ end
52
+
53
+ test 'collection radio accepts html options as input' do
54
+ form_for @user do |f|
55
+ concat f.collection_radio :active, [[1, true], [0, false]], :last, :first, {}, :class => 'radio'
56
+ end
57
+
58
+ assert_select 'form input[type=radio][value=true].radio#user_active_true'
59
+ assert_select 'form input[type=radio][value=false].radio#user_active_false'
60
+ end
61
+
62
+ # COLLECTION CHECK BOX
63
+ test 'collection check box accepts a collection and generate a serie of checkboxes for value method' do
64
+ collection = [Tag.new(1, 'Tag 1'), Tag.new(2, 'Tag 2')]
65
+ form_for @user do |f|
66
+ concat f.collection_check_boxes :tag_ids, collection, :id, :name
67
+ end
68
+
69
+ assert_select "form input[type=hidden][name='user[tag_ids][]'][value=]"
70
+ assert_select 'form input#user_tag_ids_1[type=checkbox][value=1]'
71
+ assert_select 'form input#user_tag_ids_2[type=checkbox][value=2]'
72
+ end
73
+
74
+ test 'collection check box accepts a collection and generate a serie of checkboxes with labels for label method' do
75
+ collection = [Tag.new(1, 'Tag 1'), Tag.new(2, 'Tag 2')]
76
+ form_for @user do |f|
77
+ concat f.collection_check_boxes :tag_ids, collection, :id, :name
78
+ end
79
+
80
+ assert_select 'form label.collection_check_boxes[for=user_tag_ids_1]', 'Tag 1'
81
+ assert_select 'form label.collection_check_boxes[for=user_tag_ids_2]', 'Tag 2'
82
+ end
83
+
84
+ test 'collection check box accepts selected values as :checked option' do
85
+ collection = (1..3).map{|i| [i, "Tag #{i}"] }
86
+ form_for @user do |f|
87
+ concat f.collection_check_boxes :tag_ids, collection, :first, :last, :checked => [1, 3]
88
+ end
89
+
90
+ assert_select 'form input[type=checkbox][value=1][checked=checked]'
91
+ assert_select 'form input[type=checkbox][value=3][checked=checked]'
92
+ assert_no_select 'form input[type=checkbox][value=2][checked=checked]'
93
+ end
94
+
95
+ test 'collection check box accepts a single checked value' do
96
+ collection = (1..3).map{|i| [i, "Tag #{i}"] }
97
+ form_for @user do |f|
98
+ concat f.collection_check_boxes :tag_ids, collection, :first, :last, :checked => 3
99
+ end
100
+
101
+ assert_select 'form input[type=checkbox][value=3][checked=checked]'
102
+ assert_no_select 'form input[type=checkbox][value=1][checked=checked]'
103
+ assert_no_select 'form input[type=checkbox][value=2][checked=checked]'
104
+ end
105
+
106
+ test 'collection check box accepts multiple disabled items' do
107
+ collection = (1..3).map{|i| [i, "Tag #{i}"] }
108
+ form_for @user do |f|
109
+ concat f.collection_check_boxes :tag_ids, collection, :first, :last, :disabled => [1, 3]
110
+ end
111
+
112
+ assert_select 'form input[type=checkbox][value=1][disabled=disabled]'
113
+ assert_select 'form input[type=checkbox][value=3][disabled=disabled]'
114
+ assert_no_select 'form input[type=checkbox][value=2][disabled=disabled]'
115
+ end
116
+
117
+ test 'collection check box accepts single disable item' do
118
+ collection = (1..3).map{|i| [i, "Tag #{i}"] }
119
+ form_for @user do |f|
120
+ concat f.collection_check_boxes :tag_ids, collection, :first, :last, :disabled => 1
121
+ end
122
+
123
+ assert_select 'form input[type=checkbox][value=1][disabled=disabled]'
124
+ assert_no_select 'form input[type=checkbox][value=3][disabled=disabled]'
125
+ assert_no_select 'form input[type=checkbox][value=2][disabled=disabled]'
126
+ end
127
+
128
+ test 'collection check box accepts a proc to disabled items' do
129
+ collection = (1..3).map{|i| [i, "Tag #{i}"] }
130
+ form_for @user do |f|
131
+ concat f.collection_check_boxes :tag_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
132
+ end
133
+
134
+ assert_select 'form input[type=checkbox][value=1][disabled=disabled]'
135
+ assert_no_select 'form input[type=checkbox][value=3][disabled=disabled]'
136
+ assert_no_select 'form input[type=checkbox][value=2][disabled=disabled]'
137
+ end
138
+
139
+ test 'collection check box accepts html options' do
140
+ collection = [[1, 'Tag 1'], [2, 'Tag 2']]
141
+ form_for @user do |f|
142
+ concat f.collection_check_boxes :tag_ids, collection, :first, :last, {}, :class => 'check'
143
+ end
144
+
145
+ assert_select 'form input.check[type=checkbox][value=1]'
146
+ assert_select 'form input.check[type=checkbox][value=2]'
147
+ end
148
+
149
+ test 'collection check box with fields for' do
150
+ collection = [Tag.new(1, 'Tag 1'), Tag.new(2, 'Tag 2')]
151
+ form_for @user do |f|
152
+ f.fields_for :post do |p|
153
+ concat p.collection_check_boxes :tag_ids, collection, :id, :name
154
+ end
155
+ end
156
+
157
+ assert_select 'form input#user_post_tag_ids_1[type=checkbox][value=1]'
158
+ assert_select 'form input#user_post_tag_ids_2[type=checkbox][value=2]'
159
+
160
+ assert_select 'form label.collection_check_boxes[for=user_post_tag_ids_1]', 'Tag 1'
161
+ assert_select 'form label.collection_check_boxes[for=user_post_tag_ids_2]', 'Tag 2'
162
+ end
163
+
164
+ # SIMPLE FIELDS
165
+ test 'simple fields for is available and yields an instance of FormBuilder' do
166
+ form_for @user do |f|
167
+ f.simple_fields_for :posts do |posts_form|
168
+ assert posts_form.instance_of?(SimpleForm::FormBuilder)
169
+ end
170
+ end
171
+ end
172
+ end