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 +109 -0
- data/TODO +0 -1
- data/javascript/lib/client_side_validations.js +38 -28
- data/lib/client_side_validations.rb +37 -40
- data/lib/client_side_validations/adapters/action_view.rb +57 -0
- data/lib/{adapters → client_side_validations/adapters}/active_model.rb +15 -3
- data/lib/{adapters → client_side_validations/adapters}/active_record_2.rb +8 -0
- data/lib/client_side_validations/orm.rb +45 -0
- data/lib/client_side_validations/template.rb +3 -0
- data/lib/generators/client_side_validations_generator.rb +13 -0
- metadata +26 -47
- data/.document +0 -5
- data/.gitignore +0 -22
- data/README.rdoc +0 -7
- data/Rakefile +0 -46
- data/VERSION +0 -1
- data/client_side_validations.gemspec +0 -80
- data/javascript/jspec/commands/example_command.rb +0 -19
- data/javascript/jspec/rhino.js +0 -10
- data/javascript/jspec/unit/jquery.validate.spec.js +0 -146
- data/javascript/jspec/unit/spec.helper.js +0 -0
- data/javascript/vendor/jspec.js +0 -1889
- data/javascript/vendor/jspec.xhr.js +0 -210
- data/lib/adapters/action_view.rb +0 -39
- data/spec/action_view_2_spec.rb +0 -55
- data/spec/action_view_3_spec.rb +0 -55
- data/spec/active_model_3_spec.rb +0 -271
- data/spec/active_record_2_spec.rb +0 -286
- data/spec/spec.opts +0 -1
- data/spec/spec_helper.rb +0 -9
- data/spec/support/required_gems.rb +0 -2
- data/tasks/spec.rake +0 -31
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,33 +1,36 @@
|
|
1
|
-
/* Additional jQueryValidator methods */
|
2
|
-
|
3
1
|
if (typeof(jQuery) != "undefined") {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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'
|
1
|
+
require 'rubygems'
|
2
|
+
require 'json'
|
3
|
+
require 'cgi'
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
39
|
-
|
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('{
|
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('{
|
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
|