caring_form 1.2.3

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +17 -0
  4. data/Appraisals +18 -0
  5. data/Gemfile +9 -0
  6. data/Guardfile +15 -0
  7. data/LICENSE +22 -0
  8. data/README.md +33 -0
  9. data/Rakefile +12 -0
  10. data/app/assets/javascripts/caring_form/form-observer.coffee +63 -0
  11. data/app/assets/javascripts/caring_form/main.coffee +15 -0
  12. data/app/assets/javascripts/caring_form/remote-validator.coffee +71 -0
  13. data/app/assets/javascripts/caring_form/rule-based-validator.coffee +37 -0
  14. data/app/assets/javascripts/caring_form/setup.coffee +5 -0
  15. data/app/assets/javascripts/caring_form/webshims-customization.coffee +25 -0
  16. data/app/assets/javascripts/caring_forms.js +7 -0
  17. data/caring_form.gemspec +31 -0
  18. data/gemfiles/rails_3_1.gemfile +8 -0
  19. data/gemfiles/rails_3_2.gemfile +8 -0
  20. data/gemfiles/rails_4.gemfile +7 -0
  21. data/gemfiles/rails_4_1.gemfile +8 -0
  22. data/lib/caring_form.rb +20 -0
  23. data/lib/caring_form/action_view_extensions/builder.rb +75 -0
  24. data/lib/caring_form/action_view_extensions/config.rb +4 -0
  25. data/lib/caring_form/action_view_extensions/flash_error_helper.rb +28 -0
  26. data/lib/caring_form/action_view_extensions/form_helper.rb +33 -0
  27. data/lib/caring_form/active_model/monkey_patching.rb +32 -0
  28. data/lib/caring_form/dsl.rb +91 -0
  29. data/lib/caring_form/engine.rb +4 -0
  30. data/lib/caring_form/field/base.rb +145 -0
  31. data/lib/caring_form/field/check_box.rb +23 -0
  32. data/lib/caring_form/field/check_boxes.rb +15 -0
  33. data/lib/caring_form/field/collection.rb +48 -0
  34. data/lib/caring_form/field/dummy.rb +30 -0
  35. data/lib/caring_form/field/email.rb +41 -0
  36. data/lib/caring_form/field/full_name.rb +40 -0
  37. data/lib/caring_form/field/looking_for.rb +37 -0
  38. data/lib/caring_form/field/metadata.rb +26 -0
  39. data/lib/caring_form/field/radio_buttons.rb +15 -0
  40. data/lib/caring_form/field/string.rb +13 -0
  41. data/lib/caring_form/field/submit.rb +83 -0
  42. data/lib/caring_form/field/tel.rb +37 -0
  43. data/lib/caring_form/field/text.rb +13 -0
  44. data/lib/caring_form/field/us_zip_code.rb +37 -0
  45. data/lib/caring_form/field_container.rb +126 -0
  46. data/lib/caring_form/form_builder.rb +51 -0
  47. data/lib/caring_form/model.rb +58 -0
  48. data/lib/caring_form/model/active_model.rb +11 -0
  49. data/lib/caring_form/model/active_record.rb +38 -0
  50. data/lib/caring_form/simple_form/initializer.rb +26 -0
  51. data/lib/caring_form/simple_form/monkey_patching.rb +51 -0
  52. data/lib/caring_form/tools/referee.rb +62 -0
  53. data/lib/caring_form/version.rb +3 -0
  54. data/lib/tasks/webshims.rake +7 -0
  55. data/spec/caring_form/action_view_extensions/flash_error_helper_spec.rb +50 -0
  56. data/spec/caring_form/action_view_extensions/form_helper_spec.rb +72 -0
  57. data/spec/caring_form/dsl_spec.rb +21 -0
  58. data/spec/caring_form/field/base_spec.rb +204 -0
  59. data/spec/caring_form/field/check_boxes_spec.rb +32 -0
  60. data/spec/caring_form/field/collection_spec.rb +28 -0
  61. data/spec/caring_form/field/radio_buttons_spec.rb +29 -0
  62. data/spec/caring_form/field/submit_spec.rb +57 -0
  63. data/spec/caring_form/field_container_spec.rb +85 -0
  64. data/spec/caring_form/form_builder_spec.rb +113 -0
  65. data/spec/caring_form/model_spec.rb +235 -0
  66. data/spec/caring_form/tools/referee_spec.rb +86 -0
  67. data/spec/spec_helper.rb +28 -0
  68. data/spec/support/nokogiri_matcher.rb +204 -0
  69. metadata +278 -0
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~>4.0.0"
6
+
7
+ gemspec :path=>"../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.1.1"
6
+ gem "simple_form", "~> 3.1.0.rc1"
7
+
8
+ gemspec :path=>"../"
@@ -0,0 +1,20 @@
1
+ require "caring_form/version"
2
+ require 'caring_form/simple_form/initializer'
3
+ require 'caring_form/simple_form/monkey_patching'
4
+ require 'caring_form/active_model/monkey_patching'
5
+
6
+ if defined?(Rails)
7
+ require 'caring_form/engine'
8
+ require "webshims-rails"
9
+ end
10
+
11
+ module CaringForm
12
+ Error = Class.new(StandardError)
13
+ end
14
+
15
+ require 'caring_form/model'
16
+ require 'caring_form/form_builder'
17
+ require 'caring_form/action_view_extensions/form_helper'
18
+ require 'caring_form/action_view_extensions/builder'
19
+ require 'caring_form/action_view_extensions/config'
20
+ require 'caring_form/action_view_extensions/flash_error_helper'
@@ -0,0 +1,75 @@
1
+ module CaringForm
2
+ module ActionViewExtensions
3
+ module Builder
4
+ def collection_caring_radio(*args)
5
+ collection_caring(:radio_button, *args)
6
+ end
7
+
8
+ def collection_caring_check(attribute, collection, value, text, options = {}, html_options = {})
9
+ if html_options[:required] && options[:collection] && options[:collection].length > 1
10
+ html_options.delete(:required)
11
+ html_options[:class] = "group-required #{html_options[:class]}".strip
12
+ end
13
+
14
+ collection_caring(:check_box, attribute, collection, value, text, options, html_options)
15
+ end
16
+
17
+ private
18
+
19
+ def collection_caring(input_helper, attribute, collection, value_method, text_method, options={}, html_options={})
20
+ collection.inject('') do |result, item|
21
+ value = item.send value_method
22
+ text = item.send text_method
23
+
24
+ default_html_options = begin
25
+ if self.respond_to? :default_html_options_for_collection
26
+ default_html_options_for_collection(item, value, options, html_options)
27
+ else
28
+ @default_options.merge(html_options)
29
+ end
30
+ end
31
+
32
+ label_options = {:class => 'rhs'}
33
+
34
+ if default_html_options.has_key?(:id)
35
+ value = value.gsub(/\s+/, '').underscore
36
+ default_html_options[:id] = "#{default_html_options[:id]}_#{value}"
37
+ label_options[:for] = default_html_options[:id]
38
+ label_name = nil
39
+ else
40
+ label_name = "#{attribute}_#{value}"
41
+ end
42
+
43
+ html = if input_helper == :radio_button
44
+ radio_button(attribute, value, default_html_options)
45
+ else
46
+ default_html_options[:multiple] = true
47
+ check_box(attribute, default_html_options, value, '')
48
+ end
49
+ result << html
50
+ result << if label_name.nil?
51
+ label(:not_used, text, label_options)
52
+ else
53
+ label(label_name, text, label_options)
54
+ end
55
+ result << '<br />'.html_safe
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ ActionView::Helpers::FormBuilder.send :include, CaringForm::ActionViewExtensions::Builder
63
+
64
+ require 'simple_form'
65
+ module SimpleForm
66
+ class FormBuilder
67
+ if defined?(SimpleForm::Inputs::CollectionRadioButtonsInput)
68
+ map_type :caring_radio, :to => SimpleForm::Inputs::CollectionRadioButtonsInput
69
+ map_type :caring_check, :to => SimpleForm::Inputs::CollectionCheckBoxesInput
70
+ else
71
+ map_type :caring_radio, :to => SimpleForm::Inputs::CollectionInput
72
+ map_type :caring_check, :to => SimpleForm::Inputs::CollectionInput
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ # don't render those field_with_error wrappers
2
+ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
3
+ html_tag
4
+ end
@@ -0,0 +1,28 @@
1
+ module CaringForm
2
+ module ActionViewExtensions
3
+ module FlashErrorHelper
4
+ def caring_form_errors_for(form)
5
+ return '' if form.valid?
6
+ header, message = form.error_config.values_at(:header, :message)
7
+
8
+ content_tag(:div, :class => 'flash error') do
9
+ content_tag(:h2, header) +
10
+ content_tag(:div, :class => 'flash-close') do
11
+ content_tag(:a, "Close", :href => 'javascript:void(0)')
12
+ end +
13
+ content_tag(:div, :class => 'message') do
14
+ message.html_safe + content_tag(:ul) do
15
+ form.errors.values.flatten.map do |error_message|
16
+ content_tag(:li, error_message)
17
+ end.join.html_safe
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ if defined?(ActionView)
27
+ ActionView::Base.send :include, CaringForm::ActionViewExtensions::FlashErrorHelper
28
+ end
@@ -0,0 +1,33 @@
1
+ # TODO: be more flexible for URL generation (set form object #to_param, #persisted?, etc... to rely on rails conventions)
2
+ module CaringForm
3
+ module ActionViewExtensions
4
+ module FormHelper
5
+ def caring_form_for(form_object, *args, &block)
6
+ block ||= proc {}
7
+ options = args.extract_options!
8
+ options[:builder] = CaringForm::FormBuilder
9
+ options[:url] = options.fetch(:url) do
10
+ form_object.url.present? ? form_object.url : url_for(params)
11
+ end
12
+ options[:html] ||= {}
13
+ options[:html][:class] = class_value(form_object, options[:html][:class])
14
+ options[:html][:id] ||= form_object.id unless form_object.id.nil?
15
+ args << options
16
+ form_for(form_object, *args, &block)
17
+ end
18
+
19
+ private
20
+
21
+ def class_value(form, extra_classes)
22
+ classes = %w(caring-form form-text simple_form)
23
+ classes.push(form.classes) if form.classes.present?
24
+ classes.push(extra_classes) if extra_classes.present?
25
+ classes.join(' ')
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ if defined?(ActionView)
32
+ ActionView::Base.send :include, CaringForm::ActionViewExtensions::FormHelper
33
+ end
@@ -0,0 +1,32 @@
1
+ require 'active_model'
2
+
3
+ if ActiveModel::VERSION::MAJOR == 3 && ActiveModel::VERSION::MINOR == 0
4
+
5
+ require 'active_model/validations/inclusion'
6
+
7
+ # backport inclusion validation from v3.1+ where :in can be a lambda/proc
8
+ class ActiveModel::Validations::InclusionValidator
9
+ ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " <<
10
+ "and must be supplied as the :in option of the configuration hash"
11
+
12
+ def check_validity!
13
+ unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) }
14
+ raise ArgumentError, ERROR_MESSAGE
15
+ end
16
+ end
17
+
18
+ def validate_each(record, attribute, value)
19
+ delimiter = options[:in]
20
+ exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
21
+ unless exclusions.send(inclusion_method(exclusions), value)
22
+ record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value))
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def inclusion_method(enumerable)
29
+ enumerable.is_a?(Range) ? :cover? : :include?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,91 @@
1
+ require 'caring_form/field_container'
2
+
3
+ module CaringForm
4
+ module Dsl
5
+ def self.included(klass)
6
+ klass.instance_eval do
7
+ include CaringForm::FieldContainer
8
+ extend ClassMethods
9
+ end
10
+ end
11
+
12
+ def autofocus?(name)
13
+ self.class.autofocus == name
14
+ end
15
+
16
+ def index_on
17
+ return nil unless (index_name = self.class.index_on)
18
+ return nil unless respond_to?(index_name)
19
+ value = send(index_name)
20
+ value.present? ? value : nil
21
+ end
22
+
23
+ def spam?
24
+ self.class.can_check_spam? && reference.present?
25
+ end
26
+
27
+ def attributes(options = {})
28
+ keys = if options.has_key?(:except)
29
+ except = options[:except]
30
+ field_names do |name, field|
31
+ except.none? { |key, value| field[key] == value }
32
+ end
33
+ else
34
+ field_names
35
+ end
36
+ keys.each_with_object({}) do |key, hash|
37
+ next unless respond_to?(key)
38
+ value = send(key)
39
+ hash[key.to_sym] = value unless value.nil?
40
+ end
41
+ end
42
+
43
+ def error_config
44
+ self.class.error_config
45
+ end
46
+
47
+ module ClassMethods
48
+ [:autofocus, :index_on, :id, :url, :classes].each do |variable|
49
+ variable_name = :"@#{variable}"
50
+ define_method(variable) do |*names|
51
+ instance_variable_set(variable_name, names.first) unless names.empty?
52
+ instance_variable_get(variable_name)
53
+ end
54
+ end
55
+
56
+ def protect_from_bots
57
+ dummy(:reference, :ancillary => true)
58
+ @can_check_spam = true
59
+ end
60
+
61
+ def can_check_spam?
62
+ @can_check_spam == true
63
+ end
64
+
65
+ def on_error(options = {})
66
+ @error_config = options.slice(:header, :message)
67
+ end
68
+
69
+ def error_config
70
+ @error_config ||= {
71
+ :header => "Sorry, this operation failed",
72
+ :message => "Please fix the errors below and try again."
73
+ }
74
+ end
75
+
76
+ def method_missing(meth, *args, &blk)
77
+ if CaringForm::FieldContainer::field_for(meth)
78
+ singleton = (class << self; self; end)
79
+ singleton.send(:define_method, meth) do |*meth_args|
80
+ options = meth_args.extract_options!.merge(:type => meth)
81
+ meth_args << nil if meth_args.empty? # set name to nil if no name
82
+ field(*(meth_args << options))
83
+ end
84
+ send(meth, *args)
85
+ else
86
+ super
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,4 @@
1
+ module CaringForm
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,145 @@
1
+ # TODO: remove :wrapper that's only used to wrap a submit (use wrapper_tag instead?)
2
+ # TODO: replace :required_message with a more generic way of overriding built-in field validation messages
3
+ # TODO: allow to override validation message in the UI (warning: many validation rules on server side but only one validation message on client side)
4
+ # TODO: document the API: which fields accept which options?
5
+ # TODO: move non generic fields to their own place (looking_for, us_zip_code, etc...)
6
+ # TODO: make sure we pass through all options we don't handle to simple_form so caring_form builder is a subset of simple_form builder
7
+ # TODO: make sure we have a clear pattern to override field defaults, field declaration (form object) and field options (view) and that we use it everywhere
8
+ # TODO: use locale to store default messages such as default validation messages
9
+
10
+ require 'caring_form/tools/referee'
11
+
12
+ module CaringForm
13
+ module Field
14
+ def self.field_attributes
15
+ [
16
+ :type, :hint, :optional, :label, :pattern, :remote_validator,
17
+ :collection, :wrapper, :wrapper_tag, :wrapper_html, :input_html,
18
+ :input_tag, :label_html, :ancillary, :primary, :prompt, :as, :if,
19
+ :required_message, :value
20
+ ]
21
+ end
22
+
23
+ Base = Struct.new(:name, *CaringForm::Field.field_attributes) do
24
+ def self.register(type)
25
+ CaringForm::FieldContainer.register_field(self, type)
26
+ end
27
+
28
+ def initialize(*args)
29
+ super
30
+
31
+ self.type ||= :string
32
+ self.optional ||= false
33
+ self.input_tag ||= 'input'
34
+ self.wrapper_tag ||= 'li'
35
+ self.wrapper_html ||= {}
36
+ self.input_html ||= {}
37
+ self.label_html ||= {}
38
+ self.ancillary ||= false
39
+ self.required_message ||= "Enter the #{name.to_s.humanize.downcase}"
40
+
41
+ self.label = default_label if self.label.nil?
42
+
43
+ @referee = Tools::Referee.new(self)
44
+ end
45
+
46
+ def apply_to_model(klass)
47
+ klass.send(:attr_accessor, name)
48
+ options = {:message => required_message}
49
+ if @referee.is_rule?(:optional)
50
+ referee = @referee
51
+ options[:if] = lambda do |form|
52
+ !referee.decide(:optional, form)
53
+ end
54
+ klass.send(:validates_presence_of, name, options)
55
+ elsif !optional
56
+ options[:if] = self.if if self.if.present?
57
+ klass.send(:validates_presence_of, name, options)
58
+ end
59
+ end
60
+
61
+ def input_properties(form, options = {})
62
+ options = normalize_options(form, options)
63
+ req = {
64
+ :as => options.fetch(:as, simple_type),
65
+ :collection => options.fetch(:collection, collection),
66
+ :prompt => options.fetch(:prompt, prompt),
67
+ :label => options.fetch(:label, label),
68
+ :wrapper => options.fetch(:wrapper, wrapper),
69
+ :wrapper_tag => options.fetch(:wrapper_tag, wrapper_tag),
70
+ :wrapper_html => options.fetch(:wrapper_html, wrapper_html).merge(:class => 'field'),
71
+ :input_html => options.fetch(:input_html, input_html),
72
+ :label_html => options.fetch(:label_html, label_html)
73
+ }
74
+ req[:input_html][:autofocus] = 'autofocus' if form.autofocus?(name)
75
+ req[:input_html][:title] ||= required_message if required_message.present?
76
+ if form.errors[self.name].present?
77
+ req[:wrapper_html][:class] = "field-with-errors #{req[:wrapper_html][:class]}".rstrip
78
+ end
79
+
80
+ if form.index_on.present?
81
+ req[:label_html][:index] = req[:input_html][:index] = form.index_on
82
+ end
83
+ if @referee.decide(:optional, form)
84
+ req[:required] = false
85
+ else
86
+ req[:input_html][:required] = 'required'
87
+ end
88
+ req[:input_html].merge!(@referee.rule_as_data_attribute(:optional, form))
89
+ req[:input_html][:name] = name if ancillary
90
+ set_hint!(req)
91
+ set_remote_validator!(req)
92
+ req.deep_merge!(options)
93
+ end
94
+
95
+ def simple_type
96
+ raise "override me!"
97
+ end
98
+
99
+ def default_label
100
+ self.name.to_s.humanize.titleize
101
+ end
102
+
103
+ def render(form, builder, options = {})
104
+ properties = input_properties(form, options)
105
+ if properties[:generate] == false
106
+ ''
107
+ else
108
+ properties[:required] = true unless optional
109
+ builder.input(name, properties)
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def normalize_options(form, options)
116
+ options = options.dup
117
+ [:label, :collection].each do |name|
118
+ value = options.fetch(name, send(name))
119
+ options[name] = form.send(value) if value.is_a?(Symbol)
120
+ end
121
+ if_value = options.fetch(:if, self.if)
122
+ options[:generate] = form.send(if_value) if if_value.is_a?(Symbol)
123
+ options
124
+ end
125
+
126
+ def set_hint!(req)
127
+ return unless hint.present?
128
+ req[:hint] = hint
129
+ end
130
+
131
+ def set_remote_validator!(req)
132
+ return unless remote_validator.present?
133
+ req[:as] = :string_inline_error
134
+ url, idle = if remote_validator.respond_to?(:values_at)
135
+ remote_validator.values_at(:url, :idle_time)
136
+ else
137
+ [remote_validator, nil]
138
+ end
139
+ req[:input_html][:'data-validator-url'] = url
140
+ req[:input_html][:'data-max-idle-time'] = idle if idle
141
+ req[:input_html][:class] = "validate-remotely #{req[:input_html][:class]}".strip
142
+ end
143
+ end
144
+ end
145
+ end