client_side_validations 0.8.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,109 @@
1
+ # Client Side Validations
2
+ Now you can easily drop in client side validations in any Rails app. It will use validations defined in a given ActiveRecord (or ActiveModel) class for use with a Javascript form validator. (currently only [jquery.validate](http://bassistance.de/jquery-plugins/jquery-plugin-validation/) is supported)
3
+
4
+ For Rails 2 and Rails 3 example apps please see [client_side_validations_example](http://github.com/dnclabs/client_side_validations_examples)
5
+
6
+ The concept is simple:
7
+
8
+ 1. Include the middleware
9
+ 2. Define validations in the model as you normally would
10
+ 3. The validations are sent to the client in JSON
11
+ 4. client_side_validations.js converts the JSON for a given validation plugin and binds the validator to the form
12
+
13
+ Currently the following validations are supported:
14
+
15
+ * validates_presence_of
16
+ * validates_format_of
17
+ * validates_numericality_of
18
+ * validates_length_of
19
+ * validates_uniqueness_of
20
+
21
+ The uniqueness validation works for both ActiveRecord and Mongoid.
22
+
23
+ ## Installation
24
+ > gem install client_side_validations
25
+
26
+ ### Rails 2
27
+ Add "config.gem :client_side_validations" to the "config/environment.rb" file
28
+
29
+ Then run the generator:
30
+ > script/generate client_side_validations
31
+
32
+ This will copy client_side_validations.js to "public/javascripts"
33
+
34
+ ### Rails 3
35
+ Add "gem 'client_side_validations" to the Gemfile
36
+
37
+ Then run the generator:
38
+ > rails g client_side_validations
39
+
40
+ This will copy client_side_validations.js to "public/javascripts"
41
+
42
+ ## Configuration
43
+ Currently only [jquery.validate](http://bassistance.de/jquery-plugins/jquery-plugin-validation/) is supported so you will need to download [jQuery](http://docs.jquery.com/Downloading_jQuery) and the jQuery Validate plugin to "public/javascripts"
44
+
45
+ ### Rack
46
+ The following routes will be reserved for client side validations:
47
+
48
+ /validations.json
49
+ /validations/uniqueness.json
50
+
51
+ Add the middleware to your stack:
52
+
53
+ config/environment.rb for Rails 2.x
54
+
55
+ config/application.rb for Rails 3.x
56
+
57
+ ...
58
+ config.middleware.use 'ClientSideValidations'
59
+ ...
60
+
61
+ ### Model
62
+ Validate your models as you normally would
63
+
64
+ class Book < ActiveRecord::Base
65
+ validates_presence_of :author
66
+ end
67
+
68
+ ### Layout
69
+ You currently need both jQuery and the jQuery Validate plugin loaded before you load Client Side Validations
70
+
71
+ ...
72
+ <%= javascript_include_tag 'jquery', 'jquery.validate', 'client_side_validations' %>
73
+ ...
74
+
75
+ ### View
76
+ Have a form ask for client side validations by passing :validate => true
77
+
78
+ ...
79
+
80
+ <% form_for @book, :validations => true do |b| %>
81
+ <%= b.label :author %></br>
82
+ <%= b.text_field :author %></br>
83
+ <%= submit_tag 'Create' %>
84
+ <% end %>
85
+
86
+ ...
87
+
88
+ That should be it!
89
+
90
+ ## Advanced Options
91
+
92
+ ### Model
93
+ If you want to define only specific fields for client side validations just override the validation_fields method on each model
94
+
95
+ class Book < ActiveRecord::Base
96
+ validatese_presence_of :author
97
+ validates_presence_of :body
98
+
99
+ private
100
+
101
+ def validation_fields
102
+ [:author]
103
+ end
104
+ end
105
+
106
+
107
+ Written by Brian Cardarella
108
+
109
+ Copyright (c) 2010 Democratic National Committee. See LICENSE for details.
data/TODO CHANGED
@@ -1,2 +1 @@
1
- - Add Rails Generators
2
1
  - Accept options in helper to extend the given validation plugin
@@ -1,33 +1,36 @@
1
- /* Additional jQueryValidator methods */
2
-
3
1
  if (typeof(jQuery) != "undefined") {
4
- if (typeof($('').validate) != "undefined") {
5
- jQuery.validator.addMethod("format", function(value, element, params) {
6
- var pattern = new RegExp(params, "i");
7
- return this.optional(element) || pattern.test(value);
8
- }, jQuery.validator.format("Invalid format."));
9
- }
10
- }
11
-
12
- $.extend($.fn, {
13
- clientSideValidations: function(url, adapter) {
14
- var form = this;
15
- var id = form[0].id;
16
- if (/new/.test(id)) {
17
- id = /new_(\w+)/.exec(id)[1]
18
- } else if (/edit/.test(id)) {
19
- id = /edit_(\w+)_\d+/.exec(id)[1]
20
- }
21
- var client = new ClientSideValidations(id, adapter)
22
- $.getJSON(url, function(json) {
23
- var validations = client.adaptValidations(json);
24
- form.validate({
25
- rules: validations.rules,
26
- messages: validations.messages
2
+ jQuery.validator.addMethod("format", function(value, element, params) {
3
+ var pattern = new RegExp(params, "i");
4
+ return this.optional(element) || pattern.test(value);
5
+ }, jQuery.validator.format("Invalid format."));
6
+
7
+ $.extend($.fn, {
8
+ clientSideValidations: function() {
9
+ var form = this;
10
+ var object = form.attr('object-csv');
11
+ var url = '/validations.json?model=' + object;
12
+ var id = form[0].id;
13
+ var adapter = 'jquery.validate';
14
+ if (/new/.test(id)) {
15
+ id = /new_(\w+)/.exec(id)[1]
16
+ } else if (/edit/.test(id)) {
17
+ id = /edit_(\w+)_\d+/.exec(id)[1]
18
+ }
19
+ var client = new ClientSideValidations(id, adapter)
20
+ $.getJSON(url, function(json) {
21
+ var validations = client.adaptValidations(json);
22
+ form.validate({
23
+ rules: validations.rules,
24
+ messages: validations.messages
25
+ });
27
26
  });
28
- });
29
- }
30
- });
27
+ }
28
+ });
29
+
30
+ $(document).ready(function() {
31
+ $('form[object-csv]').clientSideValidations();
32
+ });
33
+ }
31
34
 
32
35
  ClientSideValidations = function(id, adapter) {
33
36
  this.id = id;
@@ -64,6 +67,13 @@ ClientSideValidations = function(id, adapter) {
64
67
  value = this.validations[attr][validation]['maximum'];
65
68
  }
66
69
  break;
70
+ case 'uniqueness':
71
+ rule = 'remote';
72
+ value = {
73
+ url: '/validations/uniqueness.json'
74
+ }
75
+ break;
76
+
67
77
  default:
68
78
  }
69
79
  if(rule == null) {
@@ -1,47 +1,44 @@
1
- require 'rubygems' unless defined?(Gem)
1
+ require 'rubygems'
2
+ require 'json'
3
+ require 'cgi'
2
4
 
3
- module DNCLabs
4
- module ClientSideValidations
5
- def validations_to_json(*attrs)
6
- hash = Hash.new { |h, attribute| h[attribute] = {} }
7
- attrs.each do |attr|
8
- hash[attr].merge!(validation_to_hash(attr))
9
- end
10
- hash.to_json
11
- end
12
-
13
- def validation_to_hash(_attr, _options = {})
14
- @dnc_csv_adapter ||= Adapter.new(self)
15
- @dnc_csv_adapter.validation_to_hash(_attr, _options)
5
+ class ClientSideValidations
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ params = CGI::parse(env['QUERY_STRING'])
12
+ case env['PATH_INFO']
13
+ when %r{^/validations.json}
14
+ body = get_validations(params['model'][0])
15
+ [200, {'Content-Type' => 'application/json', 'Content-Length' => "#{body.length}"}, body]
16
+ when %r{^/validations/uniqueness.json}
17
+ field = params.keys.first
18
+ resource, attribute = field.split(/[^\w]/)
19
+ value = params[field][0]
20
+ body = is_unique?(resource, attribute, value).to_s
21
+ [200, {'Content-Type' => 'application/json', 'Content-Length' => "#{body.length}"}, body]
22
+ else
23
+ @app.call(env)
16
24
  end
17
25
  end
18
- end
19
-
20
-
21
- # ORM
22
-
23
- if defined?(ActiveModel)
24
- require 'adapters/active_model'
25
- unless Object.respond_to?(:to_json)
26
- require 'active_support/json/encoding'
26
+
27
+ private
28
+
29
+ def get_validations(resource)
30
+ constantize_resource(resource).new.validations_to_json
27
31
  end
28
- DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveModel
29
- klass = ActiveModel::Validations
30
- elsif defined?(ActiveRecord)
31
- if ActiveRecord::VERSION::MAJOR == 2
32
- require 'adapters/active_record_2'
33
- DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveRecord2
34
- klass = ActiveRecord::Base
32
+
33
+ def is_unique?(resource, attribute, value)
34
+ constantize_resource(resource).send("find_by_#{attribute}", value) == nil
35
35
  end
36
+
37
+ def constantize_resource(resource)
38
+ eval(resource.split('_').map{ |word| word.capitalize}.join)
39
+ end
40
+
36
41
  end
37
42
 
38
- klass.class_eval do
39
- include DNCLabs::ClientSideValidations
40
- end
41
-
42
-
43
- # Template
44
-
45
- if defined?(ActionView)
46
- require 'adapters/action_view'
47
- end
43
+ require 'client_side_validations/orm'
44
+ require 'client_side_validations/template'
@@ -0,0 +1,57 @@
1
+ module DNCLabs
2
+ module ClientSideValidations
3
+ module Adapters
4
+ module ActionView
5
+ module BaseMethods
6
+
7
+ def self.included(base)
8
+ form_method = base.instance_method(:form_for)
9
+ base.class_eval do
10
+ define_method(:form_for) do |record_or_name_or_array, *args, &proc|
11
+ options = args.extract_options!
12
+ options.symbolize_keys!
13
+ if validations = options.delete(:validations)
14
+ unless options.key?(:html)
15
+ options[:html] = {}
16
+ end
17
+
18
+ unless validations == true
19
+ object = validations.to_s.underscore
20
+ else
21
+ case record_or_name_or_array
22
+ when String, Symbol
23
+ object = record_or_name_or_array.to_s
24
+ else
25
+ object = record_or_name_or_array.class.to_s.underscore
26
+ end
27
+ end
28
+
29
+ options[:html]['object-csv'] = object
30
+ end
31
+ args << options
32
+ form_method.bind(self).call(record_or_name_or_array, *args, &proc)
33
+ end
34
+ end
35
+ end
36
+
37
+ end # BaseMethods
38
+
39
+ module FormBuilderMethods
40
+
41
+ def client_side_validations(options = {})
42
+ @template.send(:client_side_validations, @object_name, objectify_options(options))
43
+ end
44
+
45
+ end # FormBuilderMethods
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ ActionView::Base.class_eval do
52
+ include DNCLabs::ClientSideValidations::Adapters::ActionView::BaseMethods
53
+ end
54
+
55
+ ActionView::Helpers::FormBuilder.class_eval do
56
+ include DNCLabs::ClientSideValidations::Adapters::ActionView::FormBuilderMethods
57
+ end
@@ -35,6 +35,10 @@ module DNCLabs
35
35
  validation_hash
36
36
  end
37
37
 
38
+ def validation_fields
39
+ base._validators.keys
40
+ end
41
+
38
42
  private
39
43
 
40
44
  def get_validation_message(validation, locale)
@@ -45,12 +49,18 @@ module DNCLabs
45
49
  I18n.translate('errors.messages.invalid', :locale => locale)
46
50
  when :length
47
51
  if count = validation.options[:minimum]
48
- I18n.translate('errors.messages.too_short', :locale => locale).sub('{{count}}', count.to_s)
52
+ I18n.translate('errors.messages.too_short', :locale => locale).sub('%{count}', count.to_s)
49
53
  elsif count = validation.options[:maximum]
50
- I18n.translate('errors.messages.too_long', :locale => locale).sub('{{count}}', count.to_s)
54
+ I18n.translate('errors.messages.too_long', :locale => locale).sub('%{count}', count.to_s)
51
55
  end
52
56
  when :numericality
53
57
  I18n.translate('errors.messages.not_a_number', :locale => locale)
58
+ when :uniqueness
59
+ if defined?(ActiveRecord) && base.kind_of?(ActiveRecord::Base)
60
+ I18n.translate('activerecord.errors.messages.taken', :locale => locale)
61
+ elsif defined?(Mongoid) && base.class.included_modules.include?(Mongoid::Document)
62
+ I18n.translate('errors.messages.taken', :locale => locale)
63
+ end
54
64
  end
55
65
 
56
66
  message = validation.options[:message]
@@ -65,10 +75,11 @@ module DNCLabs
65
75
 
66
76
  def get_validation_options(options)
67
77
  options = options.stringify_keys
68
- options.delete('on')
78
+ options.delete('on') == :create
69
79
  options.delete('tokenizer')
70
80
  options.delete('only_integer')
71
81
  options.delete('allow_nil')
82
+ options.delete('case_sensitive')
72
83
  if options['with'].kind_of?(Regexp)
73
84
  options['with'] = options['with'].inspect.to_s.sub("\\A","^").sub("\\Z","$").sub(%r{^/},"").sub(%r{/i?$}, "")
74
85
  end
@@ -82,6 +93,7 @@ module DNCLabs
82
93
  def remove_reserved_conditionals(*conditionals)
83
94
  conditionals.flatten!
84
95
  conditionals.delete_if { |conditional| conditional =~ /@_/ }
96
+ conditionals.delete_if { |conditional| conditional =~ /validation_context/ }
85
97
  conditionals.first
86
98
  end
87
99
  end
@@ -35,6 +35,10 @@ module DNCLabs
35
35
  validation_hash
36
36
  end
37
37
 
38
+ def validation_fields
39
+ base.class.reflect_on_all_validations.map { |v| v.name }.uniq
40
+ end
41
+
38
42
  private
39
43
 
40
44
  def get_validation_message(validation, locale)
@@ -51,6 +55,8 @@ module DNCLabs
51
55
  end
52
56
  when 'validates_numericality_of'
53
57
  I18n.translate('activerecord.errors.messages.not_a_number', :locale => locale)
58
+ when 'validates_uniqueness_of'
59
+ I18n.translate('activerecord.errors.messages.taken', :locale => locale)
54
60
  end
55
61
 
56
62
  message = validation.options[:message]
@@ -82,6 +88,8 @@ module DNCLabs
82
88
  'numericality'
83
89
  when 'validates_length_of'
84
90
  'length'
91
+ when 'validates_uniqueness_of'
92
+ 'uniqueness'
85
93
  end
86
94
  end
87
95
  end
@@ -0,0 +1,45 @@
1
+ module DNCLabs
2
+ module ClientSideValidations
3
+ def validations_to_json
4
+ hash = Hash.new { |h, field| h[field] = {} }
5
+ validation_fields.each do |field|
6
+ hash[field].merge!(validation_to_hash(field))
7
+ end
8
+ hash.to_json
9
+ end
10
+
11
+ def validation_to_hash(field, options = {})
12
+ dnc_csv_adapter.validation_to_hash(field, options)
13
+ end
14
+
15
+ def validation_fields
16
+ dnc_csv_adapter.validation_fields
17
+ end
18
+
19
+ def dnc_csv_adapter
20
+ unless @dnc_csv_adapter
21
+ @dnc_csv_adapter = Adapter.new(self)
22
+ end
23
+ @dnc_csv_adapter
24
+ end
25
+ end
26
+ end
27
+
28
+ if defined?(ActiveModel)
29
+ require 'client_side_validations/adapters/active_model'
30
+ DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveModel
31
+ klass = ActiveModel::Validations
32
+
33
+ elsif defined?(ActiveRecord)
34
+ if ActiveRecord::VERSION::MAJOR == 2
35
+ require 'client_side_validations/adapters/active_record_2'
36
+ DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveRecord2
37
+ klass = ActiveRecord::Base
38
+ end
39
+ end
40
+
41
+ if klass
42
+ klass.class_eval do
43
+ include DNCLabs::ClientSideValidations
44
+ end
45
+ end