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,101 @@
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
+ raise NotImplementedError,
17
+ "input should be implemented by classes inheriting from CollectionInput"
18
+ end
19
+
20
+ def input_options
21
+ options = super
22
+ options[:include_blank] = true unless skip_include_blank?
23
+ options
24
+ end
25
+
26
+ private
27
+
28
+ def collection
29
+ @collection ||= begin
30
+ collection = options.delete(:collection) || self.class.boolean_collection
31
+ collection.respond_to?(:call) ? collection.call : collection.to_a
32
+ end
33
+ end
34
+
35
+ def has_required?
36
+ super && (input_options[:include_blank] || multiple?)
37
+ end
38
+
39
+ # Check if :include_blank must be included by default.
40
+ def skip_include_blank?
41
+ (options.keys & [:prompt, :include_blank, :default, :selected]).any? || multiple?
42
+ end
43
+
44
+ def multiple?
45
+ !!options[:input_html].try(:[], :multiple)
46
+ end
47
+
48
+ # Detect the right method to find the label and value for a collection.
49
+ # If no label or value method are defined, will attempt to find them based
50
+ # on default label and value methods that can be configured through
51
+ # SimpleForm.collection_label_methods and
52
+ # SimpleForm.collection_value_methods.
53
+ def detect_collection_methods
54
+ label, value = options.delete(:label_method), options.delete(:value_method)
55
+
56
+ unless label && value
57
+ common_method_for = detect_common_display_methods
58
+ label ||= common_method_for[:label]
59
+ value ||= common_method_for[:value]
60
+ end
61
+
62
+ [label, value]
63
+ end
64
+
65
+ def detect_common_display_methods(collection_classes = detect_collection_classes)
66
+ collection_translated = translate_collection if collection_classes == [Symbol]
67
+
68
+ if collection_translated || collection_classes.include?(Array)
69
+ { :label => :first, :value => :last }
70
+ elsif collection_includes_basic_objects?(collection_classes)
71
+ { :label => :to_s, :value => :to_s }
72
+ else
73
+ sample = collection.first || collection.last
74
+
75
+ { :label => SimpleForm.collection_label_methods.find { |m| sample.respond_to?(m) },
76
+ :value => SimpleForm.collection_value_methods.find { |m| sample.respond_to?(m) } }
77
+ end
78
+ end
79
+
80
+ def detect_collection_classes(some_collection = collection)
81
+ some_collection.map { |e| e.class }.uniq
82
+ end
83
+
84
+ def collection_includes_basic_objects?(collection_classes)
85
+ (collection_classes & [
86
+ String, Integer, Fixnum, Bignum, Float, NilClass, Symbol, TrueClass, FalseClass
87
+ ]).any?
88
+ end
89
+
90
+ def translate_collection
91
+ if translated_collection = translate(:options)
92
+ @collection = collection.map do |key|
93
+ [translated_collection[key] || key, key]
94
+ end
95
+ true
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+
@@ -0,0 +1,63 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class CollectionRadioButtonsInput < CollectionInput
4
+ def input
5
+ label_method, value_method = detect_collection_methods
6
+
7
+ @builder.send("collection_#{input_type}",
8
+ attribute_name, collection, value_method, label_method,
9
+ input_options, input_html_options, &collection_block_for_nested_boolean_style
10
+ )
11
+ end
12
+
13
+ def input_options
14
+ options = super
15
+ apply_default_collection_options!(options)
16
+ apply_nested_boolean_collection_options!(options) if nested_boolean_style?
17
+ options
18
+ end
19
+
20
+ protected
21
+
22
+ def apply_default_collection_options!(options)
23
+ options[:item_wrapper_tag] ||= options.fetch(:item_wrapper_tag, SimpleForm.item_wrapper_tag)
24
+ options[:item_wrapper_class] = [
25
+ item_wrapper_class, options[:item_wrapper_class], SimpleForm.item_wrapper_class
26
+ ].compact.presence
27
+
28
+ options[:collection_wrapper_tag] ||= options.fetch(:collection_wrapper_tag, SimpleForm.collection_wrapper_tag)
29
+ options[:collection_wrapper_class] = [
30
+ options[:collection_wrapper_class], SimpleForm.collection_wrapper_class
31
+ ].compact.presence
32
+ end
33
+
34
+ # Force item wrapper to be a label when using nested boolean, to support
35
+ # configuring classes through :item_wrapper_class, and to maintain
36
+ # compatibility with :inline style and default :item_wrapper_tag.
37
+ def apply_nested_boolean_collection_options!(options)
38
+ options[:item_wrapper_tag] = :label
39
+ end
40
+
41
+ def collection_block_for_nested_boolean_style
42
+ return unless nested_boolean_style?
43
+
44
+ proc { |builder| build_nested_boolean_style_item_tag(builder) }
45
+ end
46
+
47
+ def build_nested_boolean_style_item_tag(collection_builder)
48
+ collection_builder.radio_button + collection_builder.text
49
+ end
50
+
51
+ def item_wrapper_class
52
+ "radio"
53
+ end
54
+
55
+ # Do not attempt to generate label[for] attributes by default, unless an
56
+ # explicit html option is given. This avoids generating labels pointing to
57
+ # non existent fields.
58
+ def generate_label_for_attribute?
59
+ false
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class CollectionSelectInput < CollectionInput
4
+ def input
5
+ label_method, value_method = detect_collection_methods
6
+
7
+ @builder.collection_select(
8
+ attribute_name, collection, value_method, label_method,
9
+ input_options, input_html_options
10
+ )
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
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 has_required?
9
+ false
10
+ end
11
+
12
+ private
13
+
14
+ def label_target
15
+ position = case input_type
16
+ when :date, :datetime
17
+ date_order = input_options[:order] || I18n.t('date.order')
18
+ date_order.first
19
+ else
20
+ :hour
21
+ end
22
+
23
+ position = ActionView::Helpers::DateTimeSelector::POSITION[position]
24
+ "#{attribute_name}_#{position}i"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class FileInput < Base
4
+ def input
5
+ @builder.file_field(attribute_name, input_html_options)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class GroupedCollectionSelectInput < CollectionInput
4
+ def input
5
+ label_method, value_method = detect_collection_methods
6
+ @builder.grouped_collection_select(attribute_name, grouped_collection,
7
+ group_method, group_label_method, value_method, label_method,
8
+ input_options, input_html_options)
9
+ end
10
+
11
+ private
12
+
13
+ def grouped_collection
14
+ @grouped_collection ||= begin
15
+ grouped_collection = options.delete(:collection)
16
+ grouped_collection.respond_to?(:call) ? grouped_collection.call : grouped_collection.to_a
17
+ end
18
+ end
19
+
20
+ # Sample collection
21
+ def collection
22
+ @collection ||= grouped_collection.first.try(:send, group_method) || []
23
+ end
24
+
25
+ def group_method
26
+ @group_method ||= options.delete(:group_method)
27
+ end
28
+
29
+ def group_label_method
30
+ label = options.delete(:group_label_method)
31
+
32
+ unless label
33
+ common_method_for = detect_common_display_methods(detect_collection_classes(grouped_collection))
34
+ label = common_method_for[:label]
35
+ end
36
+
37
+ label
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class HiddenInput < Base
4
+ disable :label, :errors, :hint, :required
5
+
6
+ def input
7
+ @builder.hidden_field(attribute_name, input_html_options)
8
+ end
9
+
10
+ private
11
+
12
+ def required_class
13
+ nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class NumericInput < Base
4
+ enable :placeholder, :min_max
5
+
6
+ def input
7
+ add_size!
8
+ input_html_classes.unshift("numeric")
9
+ if html5?
10
+ input_html_options[:type] ||= "number"
11
+ input_html_options[:step] ||= integer? ? 1 : "any"
12
+ end
13
+ @builder.text_field(attribute_name, input_html_options)
14
+ end
15
+
16
+ private
17
+
18
+ # Rails adds the size attr by default, if the :size key does not exist.
19
+ def add_size!
20
+ input_html_options[:size] ||= nil
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class PasswordInput < Base
4
+ enable :placeholder, :maxlength
5
+
6
+ def input
7
+ add_size!
8
+ @builder.password_field(attribute_name, input_html_options)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class PriorityInput < CollectionSelectInput
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 has_required?
16
+ false
17
+ end
18
+
19
+ def skip_include_blank?
20
+ super || input_priority.present?
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class RangeInput < NumericInput
4
+ def input
5
+ if html5?
6
+ input_html_options[:type] ||= "range"
7
+ input_html_options[:step] ||= 1
8
+ end
9
+
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class StringInput < Base
4
+ enable :placeholder, :maxlength, :pattern
5
+
6
+ def input
7
+ unless string?
8
+ input_html_classes.unshift("string")
9
+ input_html_options[:type] ||= input_type if html5?
10
+ end
11
+
12
+ add_size!
13
+ @builder.text_field(attribute_name, input_html_options)
14
+ end
15
+
16
+ private
17
+
18
+ def string?
19
+ input_type == :string
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module SimpleForm
2
+ module Inputs
3
+ class TextInput < Base
4
+ enable :placeholder, :maxlength
5
+
6
+ def input
7
+ @builder.text_area(attribute_name, input_html_options)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+
3
+ module SimpleForm
4
+ module MapType
5
+ def self.extended(base)
6
+ base.class_attribute :mappings
7
+ base.mappings = {}
8
+ end
9
+
10
+ def map_type(*types)
11
+ map_to = types.extract_options![:to]
12
+ raise ArgumentError, "You need to give :to as option to map_type" unless map_to
13
+ self.mappings = mappings.merge types.each_with_object({}) { |t, m| m[t] = map_to }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleForm
2
+ VERSION = "2.0.2.dev".freeze
3
+ end
@@ -0,0 +1,8 @@
1
+ module SimpleForm
2
+ module Wrappers
3
+ autoload :Builder, 'simple_form/wrappers/builder'
4
+ autoload :Many, 'simple_form/wrappers/many'
5
+ autoload :Root, 'simple_form/wrappers/root'
6
+ autoload :Single, 'simple_form/wrappers/single'
7
+ end
8
+ end
@@ -0,0 +1,103 @@
1
+ module SimpleForm
2
+ module Wrappers
3
+ # Provides the builder syntax for components. The builder provides
4
+ # three methods `use`, `optional` and `wrapper` and they allow the following invocations:
5
+ #
6
+ # config.wrappers do |b|
7
+ # # Use a single component
8
+ # b.use :html5
9
+ #
10
+ # # Use the component, but do not automatically lookup. It will only be triggered when
11
+ # # :placeholder is explicitly set.
12
+ # b.optional :placeholder
13
+ #
14
+ # # Use a component with specific wrapper options
15
+ # b.use :error, :wrap_with => { :tag => "span", :class => "error" }
16
+ #
17
+ # # Use a set of components by wrapping them in a tag+class.
18
+ # b.wrapper :tag => "div", :class => "another" do |ba|
19
+ # ba.use :label
20
+ # ba.use :input
21
+ # end
22
+ #
23
+ # # Use a set of components by wrapping them in a tag+class.
24
+ # # This wrapper is identified by :label_input, which means it can
25
+ # # be turned off on demand with `f.input :name, :label_input => false`
26
+ # b.wrapper :label_input, :tag => "div", :class => "another" do |ba|
27
+ # ba.use :label
28
+ # ba.use :input
29
+ # end
30
+ # end
31
+ #
32
+ # The builder also accepts default options at the root level. This is usually
33
+ # used if you want a component to be disabled by default:
34
+ #
35
+ # config.wrappers :hint => false do |b|
36
+ # b.use :hint
37
+ # b.use :label_input
38
+ # end
39
+ #
40
+ # In the example above, hint defaults to false, which means it won't automatically
41
+ # do the lookup anymore. It will only be triggered when :hint is explicitly set.
42
+ class Builder
43
+ def initialize(options)
44
+ @options = options
45
+ @components = []
46
+ end
47
+
48
+ def use(name, options=nil, &block)
49
+ if block_given?
50
+ ActiveSupport::Deprecation.warn "Passing a block to use is deprecated. " \
51
+ "Please use wrapper instead of use."
52
+ return wrapper(name, options, &block)
53
+ end
54
+
55
+ if options && options.keys != [:wrap_with]
56
+ ActiveSupport::Deprecation.warn "Passing :tag, :class and others to use is deprecated. " \
57
+ "Please invoke b.use #{name.inspect}, :wrap_with => #{options.inspect} instead."
58
+ options = { :wrap_with => options }
59
+ end
60
+
61
+ if options && wrapper = options[:wrap_with]
62
+ @components << Single.new(name, wrapper)
63
+ else
64
+ @components << name
65
+ end
66
+ end
67
+
68
+ def optional(name, options=nil, &block)
69
+ if block_given?
70
+ ActiveSupport::Deprecation.warn "Passing a block to optional is deprecated. " \
71
+ "Please use wrapper instead of optional."
72
+ return wrapper(name, options, &block)
73
+ end
74
+
75
+ if options && options.keys != [:wrap_with]
76
+ ActiveSupport::Deprecation.warn "Passing :tag, :class and others to optional is deprecated. " \
77
+ "Please invoke b.optional #{name.inspect}, :wrap_with => #{options.inspect} instead."
78
+ options = { :wrap_with => options }
79
+ end
80
+
81
+ @options[name] = false
82
+ use(name, options, &block)
83
+ end
84
+
85
+ def wrapper(name, options=nil)
86
+ if block_given?
87
+ name, options = nil, name if name.is_a?(Hash)
88
+ builder = self.class.new(@options)
89
+ options ||= {}
90
+ options[:tag] = :div if options[:tag].nil?
91
+ yield builder
92
+ @components << Many.new(name, builder.to_a, options)
93
+ else
94
+ raise ArgumentError, "A block is required as argument to wrapper"
95
+ end
96
+ end
97
+
98
+ def to_a
99
+ @components
100
+ end
101
+ end
102
+ end
103
+ end