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,126 @@
1
+ module CaringForm
2
+ module FieldContainer
3
+ def self.register_field(klass, type)
4
+ @registered_fields ||= {}
5
+ @registered_fields[type] = klass
6
+ end
7
+
8
+ def self.field_for(type)
9
+ @registered_fields[type]
10
+ end
11
+
12
+ def self.custom_field_mapping
13
+ {
14
+ :full_name => :string,
15
+ :email => :string,
16
+ :tel => :string,
17
+ :us_zip_code => :string,
18
+ :dummy => :string,
19
+ :looking_for => :select,
20
+ :metadata => :hidden
21
+ }
22
+ end
23
+
24
+ def self.included(klass)
25
+ klass.instance_eval do
26
+ extend ClassMethods
27
+ class << self
28
+ class_attribute :fields
29
+ end
30
+ self.fields = ActiveSupport::OrderedHash.new
31
+ end
32
+ end
33
+
34
+ def field_names(&block)
35
+ self.class.field_names(&block)
36
+ end
37
+
38
+ def field_definitions
39
+ self.class.fields.values.dup
40
+ end
41
+
42
+ def field_id(name)
43
+ if (field = self.class.fields[name])
44
+ id = field.input_html[:id] || default_field_id(field)
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ def default_field_id(field)
51
+ [sanitized_form_name, index_on, field.name].compact.join('_')
52
+ end
53
+
54
+ def normalize_attribute_value(name, value)
55
+ return value unless (field = self.class.fields[name.to_sym])
56
+ case
57
+ when field.type == :check_box && value.is_a?(Array)
58
+ value.last
59
+ else
60
+ value
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def sanitized_form_name
67
+ underscored_form_name = self.class.model_name.to_s.underscore
68
+ underscored_form_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
69
+ end
70
+
71
+ module ClassMethods
72
+ def field(name, options = {})
73
+ if (not_supported = options.keys - CaringForm::Field.field_attributes).present?
74
+ raise Error.new("CaringForm::Dsl.field: #{not_supported.join(', ')} not supported")
75
+ end
76
+ supported_option_names = CaringForm::Field.field_attributes
77
+ values = options.values_at(*supported_option_names)
78
+ if options[:type] == :submit
79
+ field = new_field(:submit, *values)
80
+ field.properties = Array(name)
81
+ name = :submit
82
+ else
83
+ field = new_field(name, *values)
84
+ field.apply_to_model(self)
85
+ end
86
+ @fields[name.to_sym] = field
87
+ end
88
+
89
+ def field_names(&block)
90
+ real_fields = fields.reject { |_, field| field.kind_of?(CaringForm::Field::Submit) }
91
+ return real_fields.keys.map(&:to_s) unless block_given?
92
+ selected = real_fields.select(&block)
93
+ # ruby 1.8 returns an array, 1.9 returns a hash
94
+ keys = selected.is_a?(Array) ? selected.map(&:first) : selected.keys
95
+ keys.map(&:to_s)
96
+ end
97
+
98
+ def inherited(subclass)
99
+ subclass.fields = self.fields.dup
100
+ end
101
+
102
+ private
103
+
104
+ def new_field(name, type, *values)
105
+ klass = CaringForm::FieldContainer.field_for(type)
106
+ values.unshift(type)
107
+ klass.new(name, *values)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ require 'caring_form/field/string'
114
+ require 'caring_form/field/text'
115
+ require 'caring_form/field/collection'
116
+ require 'caring_form/field/looking_for'
117
+ require 'caring_form/field/check_boxes'
118
+ require 'caring_form/field/check_box'
119
+ require 'caring_form/field/radio_buttons'
120
+ require 'caring_form/field/metadata'
121
+ require 'caring_form/field/full_name'
122
+ require 'caring_form/field/email'
123
+ require 'caring_form/field/tel'
124
+ require 'caring_form/field/us_zip_code'
125
+ require 'caring_form/field/dummy'
126
+ require 'caring_form/field/submit'
@@ -0,0 +1,51 @@
1
+ module CaringForm
2
+ class TextFieldInlineErrorField < SimpleForm::Inputs::CommonTextInput
3
+ def input
4
+ super.html_safe + '<div class="inline-error" style="display: none"></div>'.html_safe
5
+ end
6
+ end
7
+
8
+ class FormBuilder
9
+ def initialize(*args)
10
+ @form = args[1]
11
+ @builder = SimpleForm::FormBuilder.new(*args)
12
+ @builder.class.mappings[:string_inline_error] = TextFieldInlineErrorField
13
+ end
14
+
15
+ def metadata(options = {})
16
+ field(:all, options.merge(:filter_type => [:metadata, :dummy]))
17
+ end
18
+
19
+ def fields(options = {}, &block)
20
+ tag = options.delete(:tag) || :ol
21
+ @builder.template.content_tag(tag, options, &block)
22
+ end
23
+
24
+ def field(*names)
25
+ options = names.extract_options!
26
+ html_list = select_fields!(names, options).map do |field|
27
+ field.render(@form, @builder, options)
28
+ end
29
+ html_list.join("\n").html_safe
30
+ end
31
+
32
+ def method_missing(meth, *args, &blk)
33
+ return super unless @builder.respond_to?(meth)
34
+ @builder.send(meth, *args, &blk)
35
+ end
36
+
37
+ private
38
+
39
+ def select_fields!(names, options)
40
+ names = @form.field_names.map(&:to_sym) + [:submit] if names == [:all]
41
+ if options.has_key?(:except)
42
+ names -= Array(options.delete(:except))
43
+ end
44
+ types = Array(options.delete(:filter_type)) if options.has_key?(:filter_type)
45
+ @form.field_definitions.select do |field|
46
+ next false unless names.include?(field.name)
47
+ types.nil? ? true : types.include?(field.type)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,58 @@
1
+ require 'caring_form/dsl'
2
+
3
+ module CaringForm
4
+ class Model
5
+ INHERITED_ATTRIBUTES = [:url, :id, :classes]
6
+ attr_accessor *INHERITED_ATTRIBUTES
7
+
8
+ def self.inherited(klass)
9
+ klass.instance_eval do
10
+ include CaringForm::Dsl
11
+ end
12
+ end
13
+
14
+ # attributes can be scoped within key of the name of the class (posted form)
15
+ # or a flat hash of attributes
16
+ def initialize(attributes = {})
17
+ flatten_attributes(attributes).each do |name, value|
18
+ send("#{name}=", normalize_attribute_value(name, value)) if respond_to?(name)
19
+ end
20
+ (INHERITED_ATTRIBUTES - attributes.keys).each do |name|
21
+ value = self.class.send(name)
22
+ send("#{name}=", value) unless value.nil?
23
+ end
24
+ after_initialize
25
+ end
26
+
27
+ def after_initialize
28
+ # override if needed
29
+ end
30
+
31
+ def normalize_attribute_value(name, value)
32
+ value
33
+ end
34
+
35
+ private
36
+
37
+ def flatten_attributes(raw_attributes)
38
+ attributes = raw_attributes.symbolize_keys
39
+ scope_key = self.class.model_name.to_s.underscore.to_sym
40
+ # flatten scoped attributes if any
41
+ if attributes.has_key?(scope_key)
42
+ scoped = attributes.delete(scope_key)
43
+ if (index_on_name = self.class.index_on)
44
+ index = attributes[index_on_name]
45
+ scoped = scoped[index]
46
+ end
47
+ attributes.merge!(scoped.respond_to?(:merge) ? scoped : {})
48
+ end
49
+ attributes
50
+ end
51
+ end
52
+ end
53
+
54
+ if defined?(ActiveModel)
55
+ require 'caring_form/model/active_model'
56
+ else
57
+ require 'caring_form/model/active_record'
58
+ end
@@ -0,0 +1,11 @@
1
+ module CaringForm
2
+ class Model
3
+ include ActiveModel::Validations
4
+ include ActiveModel::Conversion
5
+ extend ActiveModel::Naming
6
+
7
+ def persisted?
8
+ false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ module CaringForm
2
+ class Model
3
+ attr_accessor :errors
4
+
5
+ def self.human_name
6
+ to_s.humanize
7
+ end
8
+
9
+ def self.human_attribute_name(attribute)
10
+ attribute.to_s.humanize
11
+ end
12
+
13
+ def self.self_and_descendants_from_active_record
14
+ [self]
15
+ end
16
+
17
+ def save
18
+ # TODO
19
+ end
20
+
21
+ def save!
22
+ # TODO
23
+ end
24
+
25
+ def new_record?
26
+ true
27
+ end
28
+
29
+ def update_attribute
30
+ end
31
+
32
+ include ActiveRecord::Validations
33
+
34
+ def after_initialize
35
+ @errors = ActiveRecord::Errors.new(self)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ # TODO: support only SimpleForm 2.x+ once we upgrade all our apps to Rails 3.2
2
+
3
+ require 'simple_form'
4
+
5
+ SimpleForm.setup do |config|
6
+
7
+ if config.respond_to?(:wrappers)
8
+ # SimpleForm 2+
9
+ config.wrappers :tag => :div do |b|
10
+ b.use :label
11
+ b.use :input
12
+ b.use :hint, :wrap_with => {:tag => :p, :class => 'hint'}
13
+ end
14
+ else
15
+ # SimpleForm 1
16
+ config.components = [ :label, :input, :hint ]
17
+ config.wrapper_tag = :div
18
+ config.hint_tag = :p
19
+ end
20
+
21
+ # Common
22
+ config.label_text = lambda { |label, required, explicit_label = nil|
23
+ "#{label}#{required.empty? ? ' <span class="opt">(optional)</span>' : ''}"
24
+ }
25
+
26
+ end
@@ -0,0 +1,51 @@
1
+ require 'simple_form'
2
+
3
+ module SimpleForm
4
+ module Inputs
5
+ CommonTextInput = if defined?(TextFieldInput)
6
+ TextFieldInput
7
+ elsif defined?(StringInput)
8
+ StringInput
9
+ end
10
+
11
+ Base.class_eval do
12
+ alias :orig_html_options_for :html_options_for
13
+ def html_options_for(*args)
14
+ orig_html_options_for(*args).tap do |options|
15
+ if options[:class] && options[:class].respond_to?(:split)
16
+ options[:class] = options[:class].split(' ').uniq.join(' ')
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ classes = [Base]
23
+ classes << CollectionRadioButtonsInput if defined?(CollectionRadioButtonsInput)
24
+ classes.each do |klass|
25
+ klass.class_eval do
26
+ alias :orig_input :input
27
+ def input(wrapper_options = nil)
28
+ options = input_html_options
29
+ wrapper = options.delete(:wrapper)
30
+ wrapper.nil? ? orig_input : template.content_tag(wrapper, orig_input)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ # TODO: make something like below work instead...
38
+ #
39
+ # module CaringForm
40
+ # module CanWrapInputField
41
+ # def input
42
+ # options = input_html_options
43
+ # wrapper = options.delete(:wrapper)
44
+ # content = super
45
+ # wrapper.nil? ? content : template.content_tag(wrapper, content)
46
+ # end
47
+ # end
48
+ # end
49
+
50
+ # SimpleForm::Inputs::TextFieldInput.send(:include, CaringForm::CanWrapInputField)
51
+ # SimpleForm::Inputs::CollectionInput.send(:include, CaringForm::CanWrapInputField)
@@ -0,0 +1,62 @@
1
+ module CaringForm
2
+ module Tools
3
+ # A referee is a tool used to execute rules stored in a field attribute
4
+ # against value(s) retrieved from a form instance. The referee decides
5
+ # if the rule is true or false for the form instance.
6
+ #
7
+ # i.e.:
8
+ # field = OpenStruct.new(:optional => {:absent => :band_name})
9
+ # form = OpenStruct.new(:band_name => "")
10
+ # referee = Tools::Referee.new(field)
11
+ #
12
+ # # We want to know if the field is optional
13
+ # referee.decide(:optional, form) # => true, because the band name is absent
14
+ # form.band_name = "Everything But The Girl"
15
+ # referee.decide(:optional, form) # => false, now that there is a band name, the field is not optional anymore
16
+ #
17
+ # # We also need to verify that on the front-end, so the referee can translate
18
+ # # the rule into a data attribute key value pair
19
+ # def form.field_id(name); "form_name_index_#{name}"; end
20
+ # referee.rule_as_data_attribute(:optional, form) # => data-optional-if-absent="#form_name_index_band_name"
21
+ class Referee
22
+ def initialize(field)
23
+ @field = field
24
+ end
25
+
26
+ def decide(property, form)
27
+ case (value = @field.send(property))
28
+ when true then true
29
+ when false then false
30
+ else
31
+ execute_rule(value, form)
32
+ end
33
+ end
34
+
35
+ def execute_rule(rule, form)
36
+ type = rule.keys.first
37
+ attribute = rule[type]
38
+ case type
39
+ when :absent then form.send(attribute).blank?
40
+ when :present then form.send(attribute).present?
41
+ else
42
+ false
43
+ end
44
+ end
45
+
46
+ def is_rule?(property)
47
+ @field.send(property).is_a?(Hash)
48
+ end
49
+
50
+ def rule_as_data_attribute(property, form)
51
+ rule = @field.send(property)
52
+ if rule.respond_to?(:keys) && (type = rule.keys.first)
53
+ name = :"data-#{property}-if-#{type}"
54
+ id = "##{form.field_id(rule[type])}"
55
+ {name => id}
56
+ else
57
+ {}
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ module CaringForm
2
+ VERSION = "1.2.3" unless defined?(CaringForm::VERSION)
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'fileutils'
2
+
3
+ desc "Copy the webshims to public so dynamic features will work. Only needs to run when caring_form updates the webshims."
4
+ task "webshims:update_public" do
5
+ shim_root = Webshims::Rails::Engine.root.join('vendor', 'assets', 'javascripts', 'webshims')
6
+ FileUtils.cp_r(shim_root, Rails.root.join("public/webshims"))
7
+ end
@@ -0,0 +1,50 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ require 'caring_form'
4
+
5
+ class FlashForm < CaringForm::Model
6
+ url '/'
7
+ string :name, :required_message => "Enter your name"
8
+ string :age, :required_message => "Enter your age as a number"
9
+
10
+ validates_numericality_of :age,
11
+ :greater_than_or_equal_to => 0,
12
+ :only_integer => true,
13
+ :allow_blank => true,
14
+ :message => "Age must be a number"
15
+
16
+ on_error :header => "flash header",
17
+ :message => "flash message"
18
+ end
19
+
20
+ MiniTest::Spec.register_spec_type(/CaringForm::ActionViewExtensions::FlashErrorHelper$/, ControllerSpec)
21
+
22
+ describe CaringForm::ActionViewExtensions::FlashErrorHelper do
23
+ it "renders a flash error if the form is not valid" do
24
+ view = caring_form_errors_for(FlashForm.new)
25
+ html_matcher('.flash.error > h2').must_be :matching, view
26
+ html_matcher('.flash.error > .flash-close').must_be :matching, view
27
+ html_matcher('.flash.error > .message').must_be :matching, view
28
+ html_matcher('.flash.error > .message > ul').must_be :matching, view
29
+ end
30
+
31
+ it "doesn't render a flash error if the form is valid" do
32
+ form = FlashForm.new(:name => "Zhanna", :age => "30")
33
+ caring_form_errors_for(form).must_equal ''
34
+ end
35
+
36
+ it "renders the header setup with #error_for" do
37
+ view = caring_form_errors_for(FlashForm.new)
38
+ html_matcher('.flash.error > h2', :text => "flash header").must_be :matching, view
39
+ end
40
+
41
+ it "renders a list of all the error messages" do
42
+ view = caring_form_errors_for(FlashForm.new)
43
+ html_matcher(
44
+ '.flash.error > .message li', :text => "Enter your name"
45
+ ).must_be :matching, view
46
+ html_matcher(
47
+ '.flash.error > .message li', :text => "Enter your age as a number"
48
+ ).must_be :matching, view
49
+ end
50
+ end