judge 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +273 -14
- data/app/assets/javascripts/judge.js +391 -0
- data/app/controllers/judge/validations_controller.rb +9 -0
- data/app/models/judge/validation.rb +53 -0
- data/config/routes.rb +3 -0
- data/lib/generators/judge/install/install_generator.rb +42 -0
- data/lib/judge.rb +13 -8
- data/lib/judge/config.rb +39 -0
- data/lib/judge/controller.rb +35 -0
- data/lib/judge/each_validator.rb +5 -4
- data/lib/judge/engine.rb +5 -0
- data/lib/judge/form_builder.rb +19 -24
- data/lib/judge/html.rb +5 -7
- data/lib/judge/message_collection.rb +2 -1
- data/lib/judge/message_config.rb +2 -1
- data/lib/judge/validator.rb +3 -1
- data/lib/judge/validator_collection.rb +1 -1
- data/lib/judge/version.rb +2 -2
- data/lib/tasks/judge_tasks.rake +4 -0
- data/{lib/generators/judge/templates → vendor/assets/javascripts}/json2.js +4 -5
- data/{lib/generators/judge/templates → vendor/assets/javascripts}/underscore.js +451 -285
- metadata +94 -87
- data/.gitignore +0 -9
- data/.travis.yml +0 -21
- data/Gemfile +0 -3
- data/Rakefile +0 -11
- data/judge.gemspec +0 -23
- data/lib/generators/judge/judge_generator.rb +0 -21
- data/lib/generators/judge/templates/judge.js +0 -330
- data/spec/each_validator_spec.rb +0 -17
- data/spec/form_builder_spec.rb +0 -68
- data/spec/html_spec.rb +0 -14
- data/spec/javascripts/JudgeSpec.js +0 -509
- data/spec/javascripts/fixtures/form.html +0 -538
- data/spec/javascripts/helpers/customMatchers.js +0 -20
- data/spec/javascripts/helpers/jasmine-jquery.js +0 -204
- data/spec/javascripts/helpers/jquery-1.5.1.min.js +0 -18
- data/spec/javascripts/helpers/json2.js +0 -487
- data/spec/javascripts/helpers/underscore.js +0 -1060
- data/spec/javascripts/support/jasmine.yml +0 -79
- data/spec/javascripts/support/jasmine_config.rb +0 -6
- data/spec/javascripts/support/jasmine_runner.rb +0 -21
- data/spec/javascripts/support/runner.js +0 -51
- data/spec/message_collection_spec.rb +0 -73
- data/spec/setup.rb +0 -75
- data/spec/support/factories.rb +0 -23
- data/spec/support/locale/en.yml +0 -18
- data/spec/support/setup.rb +0 -72
- data/spec/support/spec_helper.rb +0 -13
- data/spec/support/validators/city_validator.rb +0 -9
- data/spec/validator_collection_spec.rb +0 -21
- data/spec/validator_spec.rb +0 -33
data/README.md
CHANGED
@@ -1,26 +1,285 @@
|
|
1
|
-
|
2
|
-
=====
|
1
|
+
# Judge
|
3
2
|
|
4
3
|
[![Build status](https://secure.travis-ci.org/joecorcoran/judge.png?branch=master)](http://travis-ci.org/joecorcoran/judge)
|
5
4
|
|
6
|
-
|
5
|
+
Judge allows easy client side form validation for Rails 3 by porting many `ActiveModel::Validation` features to JavaScript. The most common validations work through JSON strings stored within HTML5 data attributes and are executed purely on the client side. Wherever you need to, Judge provides a simple interface for AJAX validations too.
|
7
6
|
|
8
|
-
|
9
|
-
-----
|
7
|
+
## Rationale
|
10
8
|
|
11
|
-
|
9
|
+
Whenever we need to give the user instant feedback on their form data, it's common to write some JavaScript to test form element values. Since whatever code we write to manage our data integrity in Ruby has to be copied as closely as possible in JavaScript, we end up with some very unsatisfying duplication of application logic.
|
12
10
|
|
13
|
-
|
14
|
-
----------
|
11
|
+
In many cases it would be simpler to safely expose the validation information from our models to the client – this is where Judge steps in.
|
15
12
|
|
16
|
-
|
13
|
+
## Installation
|
17
14
|
|
18
|
-
|
19
|
-
* http://github.com/joecorcoran/judge-simple_form
|
15
|
+
Judge only supports Rails 3.1 or higher.
|
20
16
|
|
21
|
-
|
22
|
-
|
17
|
+
Judge relies on [Underscore.js](underscore) in general and [JSON2.js](json2) for browsers that lack proper JSON support. If your application already makes use of these files, you can safely ignore the versions provided with Judge.
|
18
|
+
|
19
|
+
### With asset pipeline enabled
|
20
|
+
|
21
|
+
Add `judge` to your Gemfile and run `bundle install`.
|
22
|
+
|
23
|
+
Mount the engine in your routes file, as follows:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# config/routes.rb
|
27
|
+
mount Judge::Engine => '/judge'
|
28
|
+
```
|
29
|
+
|
30
|
+
Judge makes three JavaScript files available. You'll always need *judge.js* and *underscore.js*, whereas *json2.js* is only needed in older browsers. Add the following lines to *application.js*:
|
31
|
+
|
32
|
+
```
|
33
|
+
//= require underscore
|
34
|
+
//= require json2
|
35
|
+
//= require judge
|
36
|
+
```
|
37
|
+
|
38
|
+
### Without asset pipeline
|
39
|
+
|
40
|
+
Add `judge` to your Gemfile and run `bundle install`. Then run
|
41
|
+
|
42
|
+
$ rails generate judge:install path/to/your/js/dir
|
43
|
+
|
44
|
+
to copy *judge.js* to your application. There are **--json2** and **--underscore** options to copy the dependencies too.
|
45
|
+
|
46
|
+
Mount the engine in your routes file, as follows:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# config/routes.rb
|
50
|
+
mount Judge::Engine => '/judge'
|
51
|
+
```
|
52
|
+
|
53
|
+
## Getting started
|
54
|
+
|
55
|
+
Add a simple validation to your model.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Post < ActiveRecord::Base
|
59
|
+
validates :title, :presence => true
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Make sure your form uses the Judge::FormBuilder and add the :validate option to the field.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
form_for(@post, :builder => Judge::FormBuilder) do |f|
|
67
|
+
f.text_field :title, :validate => true
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
On the client side, you can now validate the title input.
|
72
|
+
|
73
|
+
```javascript
|
74
|
+
judge.validate(document.getElementById('post_title'), {
|
75
|
+
valid: function(element) {
|
76
|
+
element.style.border = '1px solid green';
|
77
|
+
},
|
78
|
+
invalid: function(element, messages) {
|
79
|
+
element.style.border = '1px solid red';
|
80
|
+
alert(messages.join(','));
|
81
|
+
}
|
82
|
+
});
|
83
|
+
```
|
84
|
+
|
85
|
+
## Judge::FormBuilder
|
86
|
+
|
87
|
+
You can use any of the methods from the standard ActionView::Helpers::FormBuilder – just add `:validate => true` to the options hash.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
f.date_select :birthday, :validate => true
|
91
|
+
```
|
92
|
+
|
93
|
+
If you need to use Judge in conjunction with your own custom `FormBuilder` methods, make sure your `FormBuilder` inherits from `Judge::FormBuilder` and use the `#add_validate_attr!` helper.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class MyFormBuilder < Judge::FormBuilder
|
97
|
+
def fancy_text_field(method, options = {})
|
98
|
+
add_validate_attr!(self.object, method, options)
|
99
|
+
# do your stuff here
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
## Available validators
|
105
|
+
|
106
|
+
* presence;
|
107
|
+
* length (options: *minimum*, *maximum*, *is*);
|
108
|
+
* exclusion (options: *in*);
|
109
|
+
* inclusion (options: *in*);
|
110
|
+
* format (options: *with*, *without*); and
|
111
|
+
* numericality (options: *greater_than*, *greater_than_or_equal_to*, *less_than*, *less_than_or_equal_to*, *equal_to*, *odd*, *even*, *only_integer*);
|
112
|
+
* acceptance;
|
113
|
+
* confirmation (input and confirmation input must have matching ids);
|
114
|
+
* uniqueness;
|
115
|
+
* any `EachValidator` that you have written, provided you add a JavaScript version too and add it to `judge.eachValidators`.
|
116
|
+
|
117
|
+
Options like *if*, *unless* and *on* are not available as they relate to record persistence.
|
118
|
+
|
119
|
+
The *allow_blank* option is available everywhere it should be. Error messages are looked up according to the [Rails i18n API](http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models).
|
120
|
+
|
121
|
+
## Validating uniqueness
|
122
|
+
|
123
|
+
In order to validate uniqueness Judge sends requests to the mounted `Judge::Engine` path, which responds with a JSON representation of an error message array. The array is empty if the value is valid.
|
124
|
+
|
125
|
+
Since this effectively means adding an open, queryable endpoint to your application, Judge is cautious and requires you to be explicit about which attributes from which models you would like to expose for validation via XHR. Allowed attributes are configurable as in the following example. Note that you are only required to do this for `uniqueness` and any other validators you write that make requests to the server.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# config/initializers/judge.rb
|
129
|
+
Judge.configure do
|
130
|
+
expose Post, :title, :body
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
## Mounting the engine at a different location
|
135
|
+
|
136
|
+
You can choose a path other than `'/judge'` if you need to; just make sure to set this on the client side too:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
# config/routes.rb
|
140
|
+
mount Judge::Engine => '/whatever'
|
141
|
+
```
|
142
|
+
|
143
|
+
```javascript
|
144
|
+
judge.enginePath = '/whatever';
|
145
|
+
```
|
146
|
+
|
147
|
+
## Writing your own `EachValidator`
|
148
|
+
|
149
|
+
If you write your own `ActiveModel::EachValidator`, Judge provides a way to ensure that your I18n error messages are available on the client side. Simply pass to `uses_messages` any number of message keys and Judge will look up the translated messages. Let's run through an example.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class FooValidator < ActiveModel::EachValidator
|
153
|
+
uses_messages :not_foo
|
154
|
+
|
155
|
+
def validate_each(record, attribute, value)
|
156
|
+
unless value == 'foo'
|
157
|
+
record.errors.add(:title, :not_foo)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
We'll use the validator in the example above to validate the title attribute of a Post object:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class Post < ActiveRecord::Base
|
167
|
+
validates :title, :foo => true
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
form_for(@post, :builder => Judge::FormBuilder) do |f|
|
173
|
+
text_field :title, :validate => true
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
Judge will look for the `not_foo` message at
|
178
|
+
*activerecord.errors.models.post.attributes.title.not_foo*
|
179
|
+
first and then onwards down the [Rails I18n lookup chain](http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models).
|
180
|
+
|
181
|
+
We then need to add our own validator method to the `judge.eachValidators` object on the client side:
|
182
|
+
|
183
|
+
```javascript
|
184
|
+
judge.eachValidators.foo = function(options, messages) {
|
185
|
+
var errorMessages = [];
|
186
|
+
// 'this' refers to the form element
|
187
|
+
if (this.value !== 'foo') {
|
188
|
+
errorMessages.push(messages.not_foo);
|
189
|
+
}
|
190
|
+
return new judge.Validation(errorMessages);
|
191
|
+
};
|
192
|
+
```
|
193
|
+
|
194
|
+
## `judge.Validation`
|
195
|
+
|
196
|
+
All client side validators must return a `Validation` – an object that can exist in three different states: *valid*, *invalid* or *pending*. If your validator function is synchronous, you can return a closed `Validation` simply by passing an array of error messages to the constructor.
|
197
|
+
|
198
|
+
```javascript
|
199
|
+
new judge.Validation([]);
|
200
|
+
// => empty array, this Validation is 'valid'
|
201
|
+
new judge.Validation(['must not be blank']);
|
202
|
+
// => array has messages, this Validation is 'invalid'
|
203
|
+
```
|
204
|
+
|
205
|
+
The *pending* state is provided for asynchronous validation; a `Validation` object we will close some time in the future. Let's look at an example, using jQuery's popular `ajax` function:
|
206
|
+
|
207
|
+
```javascript
|
208
|
+
judge.eachValidators.bar = function() {
|
209
|
+
// create a 'pending' validation
|
210
|
+
var validation = new judge.Validation();
|
211
|
+
$.ajax('/bar-checking-service').done(function(messages) {
|
212
|
+
// You can close a Validation with either an array
|
213
|
+
// or a string that represents a JSON array
|
214
|
+
validation.close(messages);
|
215
|
+
});
|
216
|
+
return validation;
|
217
|
+
};
|
218
|
+
```
|
219
|
+
|
220
|
+
There are helper functions, `judge.pending()` and `judge.closed()` for creating a new `Validation` too.
|
221
|
+
|
222
|
+
```javascript
|
223
|
+
judge.eachValidators.bar = function() {
|
224
|
+
return judge.closed(['not valid']);
|
225
|
+
};
|
226
|
+
|
227
|
+
judge.eachValidators.bar = function() {
|
228
|
+
var validation = new judge.pending();
|
229
|
+
doAsyncStuff(function(messages) {
|
230
|
+
validation.close(messages);
|
231
|
+
});
|
232
|
+
return validation;
|
233
|
+
};
|
234
|
+
```
|
235
|
+
|
236
|
+
In the unlikely event that you don't already use a library with AJAX capability, a basic function is provided for making GET requests as follows:
|
237
|
+
|
238
|
+
```javascript
|
239
|
+
judge.get('/something', {
|
240
|
+
success: function(status, headers, text) {
|
241
|
+
// status code 20x
|
242
|
+
},
|
243
|
+
error: function(status, headers, text) {
|
244
|
+
// any other status code
|
245
|
+
}
|
246
|
+
});
|
247
|
+
```
|
248
|
+
|
249
|
+
## Judge extensions
|
250
|
+
|
251
|
+
If you use [Formtastic](https://github.com/justinfrench/formtastic) or [SimpleForm](https://github.com/plataformatec/simple_form), there are extension gems to help you use Judge within your forms without any extra setup. They are essentially basic patches that add the `:validate => true` option to the `input` method.
|
252
|
+
|
253
|
+
### Formtastic
|
254
|
+
|
255
|
+
https://github.com/joecorcoran/judge-formtastic
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
gem 'judge-formtastic'
|
259
|
+
```
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
semantic_form_for(@user) do |f|
|
263
|
+
f.input :name, :validate => true
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
### SimpleForm
|
268
|
+
|
269
|
+
https://github.com/joecorcoran/judge-simple_form
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
gem 'judge-simple_form'
|
273
|
+
```
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
simple_form_for(@user) do |f|
|
277
|
+
f.input :name, :validate => true
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
## License
|
23
282
|
|
24
283
|
Released under an MIT license (see LICENSE.txt).
|
25
284
|
|
26
|
-
http://blog.joecorcoran.co.uk
|
285
|
+
[blog.joecorcoran.co.uk](http://blog.joecorcoran.co.uk) | [@josephcorcoran](http://twitter.com/josephcorcoran)
|
@@ -0,0 +1,391 @@
|
|
1
|
+
// Judge 2.0.0
|
2
|
+
// (c) 2011-2012 Joe Corcoran
|
3
|
+
// http://raw.github.com/joecorcoran/judge/master/LICENSE.txt
|
4
|
+
|
5
|
+
// This is judge.js: the JavaScript part of Judge. Judge is a client-side
|
6
|
+
// validation gem for Rails 3. You can find the Judge gem API documentation at
|
7
|
+
// <http://judge.joecorcoran.co.uk>.
|
8
|
+
|
9
|
+
(function() {
|
10
|
+
|
11
|
+
var root = this;
|
12
|
+
|
13
|
+
// The judge namespace.
|
14
|
+
var judge = root.judge = {},
|
15
|
+
_ = root._;
|
16
|
+
|
17
|
+
judge.VERSION = '2.0.0';
|
18
|
+
|
19
|
+
// Trying to be a bit more descriptive than the basic error types allow.
|
20
|
+
var DependencyError = function(message) {
|
21
|
+
this.name = 'DependencyError';
|
22
|
+
this.message = message;
|
23
|
+
};
|
24
|
+
DependencyError.prototype = new Error();
|
25
|
+
DependencyError.prototype.constructor = DependencyError;
|
26
|
+
|
27
|
+
// Throw dependency errors if necessary.
|
28
|
+
if (typeof _ === 'undefined') {
|
29
|
+
throw new DependencyError('Ensure underscore.js is loaded');
|
30
|
+
}
|
31
|
+
if (_.isUndefined(root.JSON)) {
|
32
|
+
throw new DependencyError(
|
33
|
+
'Judge depends on the global JSON object (load json2.js in old browsers)'
|
34
|
+
);
|
35
|
+
}
|
36
|
+
|
37
|
+
// Returns the object type as represented in `Object.prototype.toString`.
|
38
|
+
var objectString = function(object) {
|
39
|
+
var string = Object.prototype.toString.call(object);
|
40
|
+
return string.replace(/\[|\]/g, '').split(' ')[1];
|
41
|
+
};
|
42
|
+
|
43
|
+
// A way of checking isArray, but including weird object types that are
|
44
|
+
// returned from collection queries.
|
45
|
+
var isCollection = function(object) {
|
46
|
+
var type = objectString(object),
|
47
|
+
types = [
|
48
|
+
'Array',
|
49
|
+
'NodeList',
|
50
|
+
'StaticNodeList',
|
51
|
+
'HTMLCollection',
|
52
|
+
'HTMLFormElement',
|
53
|
+
'HTMLAllCollection'
|
54
|
+
];
|
55
|
+
return _(types).include(type);
|
56
|
+
};
|
57
|
+
|
58
|
+
// eval is used here for stuff like `(3, '<', 4) => '3 < 4' => true`.
|
59
|
+
var operate = function(input, operator, validInput) {
|
60
|
+
return eval(input+' '+operator+' '+validInput);
|
61
|
+
};
|
62
|
+
|
63
|
+
// Some nifty numerical helpers.
|
64
|
+
var
|
65
|
+
isInt = function(value) { return value === +value && value === (value|0); },
|
66
|
+
isEven = function(value) { return (value % 2 === 0) ? true : false; },
|
67
|
+
isOdd = function(value) { return !isEven(value); };
|
68
|
+
|
69
|
+
// Converts a Ruby regular expression, given as a string, into JavaScript.
|
70
|
+
// This is rudimentary at best, as there are many, many differences between
|
71
|
+
// Ruby and JavaScript when it comes to regexp-fu. The plan is to replace this
|
72
|
+
// with an XRegExp plugin which will port some Ruby regexp features to
|
73
|
+
// JavaScript.
|
74
|
+
var convertFlags = function(string) {
|
75
|
+
var on = string.split('-')[0];
|
76
|
+
return (/m/.test(on)) ? 'm' : '';
|
77
|
+
};
|
78
|
+
var convertRegExp = function(string) {
|
79
|
+
var parts = string.slice(1, -1).split(':'),
|
80
|
+
flags = parts.shift().replace('?', ''),
|
81
|
+
source = parts.join(':').replace(/\\\\/g, '\\');
|
82
|
+
return new RegExp(source, convertFlags(flags));
|
83
|
+
};
|
84
|
+
|
85
|
+
// Returns a browser-specific XHR object, or null if one cannot be constructed.
|
86
|
+
var reqObj = function() {
|
87
|
+
return (
|
88
|
+
(root.ActiveXObject && new root.ActiveXObject('Microsoft.XMLHTTP')) ||
|
89
|
+
(root.XMLHttpRequest && new root.XMLHttpRequest()) ||
|
90
|
+
null
|
91
|
+
);
|
92
|
+
};
|
93
|
+
|
94
|
+
// Performs a GET request using the browser's XHR object. This provides very
|
95
|
+
// basic ajax capability and was written specifically for use in the provided
|
96
|
+
// uniqueness validator without requiring jQuery.
|
97
|
+
var get = judge.get = function(url, callbacks) {
|
98
|
+
var req = reqObj();
|
99
|
+
if (!!req) {
|
100
|
+
req.onreadystatechange = function() {
|
101
|
+
if (req.readyState === 4) {
|
102
|
+
req.onreadystatechange = void 0;
|
103
|
+
var callback = /^20\d$/.test(req.status) ? callbacks.success : callbacks['error'];
|
104
|
+
callback(req.status, req.responseHeaders, req.responseText);
|
105
|
+
}
|
106
|
+
};
|
107
|
+
req.open('GET', url, true);
|
108
|
+
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
109
|
+
req.setRequestHeader('Accept', 'application/json');
|
110
|
+
req.send();
|
111
|
+
}
|
112
|
+
return req;
|
113
|
+
};
|
114
|
+
|
115
|
+
// Some helper methods for working with Rails-style input attributes.
|
116
|
+
var
|
117
|
+
attrFromName = function(name) {
|
118
|
+
var matches, attr = '';
|
119
|
+
if (matches = name.match(/\[(\w+)\]$/)) {
|
120
|
+
attr = matches[1];
|
121
|
+
}
|
122
|
+
return attr;
|
123
|
+
};
|
124
|
+
classFromName = function(name) {
|
125
|
+
var bracketed, klass = '';
|
126
|
+
if (bracketed = name.match(/\[(\w+)\]/g)) {
|
127
|
+
klass = (bracketed.length > 1) ? camelize(debracket(bracketed[0])) : name.match(/^\w+/)[0];
|
128
|
+
}
|
129
|
+
return klass;
|
130
|
+
};
|
131
|
+
debracket = function(str) {
|
132
|
+
return str.replace(/\[|\]/g, '');
|
133
|
+
};
|
134
|
+
camelize = function(str) {
|
135
|
+
return str.replace(/(^[a-z]|\_[a-z])/g, function($1) {
|
136
|
+
return $1.toUpperCase().replace('_','');
|
137
|
+
});
|
138
|
+
};
|
139
|
+
|
140
|
+
// Build the URL necessary to send a GET request to the mounted validations
|
141
|
+
// controller to check the validity of the given form element.
|
142
|
+
var urlFor = judge.urlFor = function(el, kind) {
|
143
|
+
var path = judge.enginePath,
|
144
|
+
params = {
|
145
|
+
'klass' : classFromName(el.name),
|
146
|
+
'attribute': attrFromName(el.name),
|
147
|
+
'value' : encodeURIComponent(el.value),
|
148
|
+
'kind' : kind
|
149
|
+
};
|
150
|
+
return encodeURI(path + queryString(params));
|
151
|
+
};
|
152
|
+
|
153
|
+
// Convert an object literal into an encoded query string.
|
154
|
+
var queryString = function(obj) {
|
155
|
+
var e = encodeURIComponent,
|
156
|
+
qs = _.reduce(obj, function(memo, value, key) {
|
157
|
+
return memo + e(key) + '=' + e(value) + '&';
|
158
|
+
}, '?');
|
159
|
+
return qs.replace(/&$/, '').replace(/%20/g, '+');
|
160
|
+
};
|
161
|
+
|
162
|
+
// Default path to mounted engine. Override this if you decide to mount
|
163
|
+
// Judge::Engine at a different location.
|
164
|
+
judge.enginePath = '/judge';
|
165
|
+
|
166
|
+
// Provides event dispatch behaviour when mixed into an object. Concept
|
167
|
+
// taken from Backbone.js, stripped down and altered.
|
168
|
+
// Backbone.js (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
169
|
+
// http://backbonejs.org
|
170
|
+
var Dispatcher = judge.Dispatcher = {
|
171
|
+
on: function(event, callback, scope) {
|
172
|
+
if (!_.isFunction(callback)) return this;
|
173
|
+
this._events || (this._events = {});
|
174
|
+
var events = this._events[event] || (this._events[event] = []);
|
175
|
+
events.push({ callback: callback, scope: scope || this });
|
176
|
+
this.trigger('bind');
|
177
|
+
return this;
|
178
|
+
},
|
179
|
+
trigger: function(event) {
|
180
|
+
if (!this._events) return this;
|
181
|
+
var args = _.rest(arguments),
|
182
|
+
events = this._events[event] || (this._events[event] = []);
|
183
|
+
_.each(events, function(event) {
|
184
|
+
event.callback.apply(event.scope, args);
|
185
|
+
});
|
186
|
+
return this;
|
187
|
+
}
|
188
|
+
};
|
189
|
+
|
190
|
+
// A queue of closed or pending Validation objects.
|
191
|
+
var ValidationQueue = judge.ValidationQueue = function(element) {
|
192
|
+
this.element = element;
|
193
|
+
this.validations = [];
|
194
|
+
this.attrValidators = root.JSON.parse(this.element.getAttribute('data-validate'));
|
195
|
+
|
196
|
+
_.each(this.attrValidators, function(av) {
|
197
|
+
if (this.element.value.length || av.options.allow_blank !== true) {
|
198
|
+
var method = _.bind(judge.eachValidators[av.kind], this.element),
|
199
|
+
validation = method(av.options, av.messages);
|
200
|
+
validation.on('close', this.tryClose, this);
|
201
|
+
this.on('bind', this.tryClose, this);
|
202
|
+
this.validations.push(validation);
|
203
|
+
}
|
204
|
+
}, this);
|
205
|
+
this.tryClose.call(this);
|
206
|
+
};
|
207
|
+
_.extend(ValidationQueue.prototype, Dispatcher, {
|
208
|
+
tryClose: function() {
|
209
|
+
var report = _.reduce(this.validations, function(obj, validation) {
|
210
|
+
obj.statuses = _.union(obj.statuses, [validation.status()]);
|
211
|
+
obj.messages = _.union(obj.messages, _.compact(validation.messages));
|
212
|
+
return obj;
|
213
|
+
}, { statuses: [], messages: [] }, this);
|
214
|
+
if (!_.contains(report.statuses, 'pending')) {
|
215
|
+
var status = _.contains(report.statuses, 'invalid') ? 'invalid' : 'valid';
|
216
|
+
this.trigger('close', this.element, status, report.messages);
|
217
|
+
this.trigger(status, this.element, report.messages);
|
218
|
+
}
|
219
|
+
}
|
220
|
+
});
|
221
|
+
|
222
|
+
// Event-capable object returned by validator methods.
|
223
|
+
var Validation = judge.Validation = function(messages) {
|
224
|
+
this.messages = null;
|
225
|
+
if (_.isArray(messages)) this.close(messages);
|
226
|
+
return this;
|
227
|
+
};
|
228
|
+
_.extend(Validation.prototype, Dispatcher, {
|
229
|
+
close: function(messages) {
|
230
|
+
if (this.closed()) return null;
|
231
|
+
this.messages = _.isString(messages) ? root.JSON.parse(messages) : messages;
|
232
|
+
this.trigger('close', this.status(), this.messages);
|
233
|
+
return this;
|
234
|
+
},
|
235
|
+
closed: function() {
|
236
|
+
return _.isArray(this.messages);
|
237
|
+
},
|
238
|
+
status: function() {
|
239
|
+
if (!this.closed()) return 'pending';
|
240
|
+
return this.messages.length > 0 ? 'invalid' : 'valid';
|
241
|
+
}
|
242
|
+
});
|
243
|
+
|
244
|
+
// Convenience methods for creating Validation objects in different states.
|
245
|
+
var pending = judge.pending = function() {
|
246
|
+
return new Validation();
|
247
|
+
};
|
248
|
+
var closed = judge.closed = function(messages) {
|
249
|
+
return new Validation(messages);
|
250
|
+
};
|
251
|
+
|
252
|
+
// Ported ActiveModel validators.
|
253
|
+
// See <http://api.rubyonrails.org/classes/ActiveModel/Validations.html> for
|
254
|
+
// the originals.
|
255
|
+
judge.eachValidators = {
|
256
|
+
// ActiveModel::Validations::PresenceValidator
|
257
|
+
presence: function(options, messages) {
|
258
|
+
return closed(this.value.length ? [] : [messages.blank]);
|
259
|
+
},
|
260
|
+
|
261
|
+
// ActiveModel::Validations::LengthValidator
|
262
|
+
length: function(options, messages) {
|
263
|
+
var msgs = [],
|
264
|
+
types = {
|
265
|
+
minimum: { operator: '<', message: 'too_short' },
|
266
|
+
maximum: { operator: '>', message: 'too_long' },
|
267
|
+
is: { operator: '!=', message: 'wrong_length' }
|
268
|
+
};
|
269
|
+
_(types).each(function(properties, type) {
|
270
|
+
var invalid = operate(this.value.length, properties.operator, options[type]);
|
271
|
+
if (_(options).has(type) && invalid) {
|
272
|
+
msgs.push(messages[properties.message]);
|
273
|
+
}
|
274
|
+
}, this);
|
275
|
+
return closed(msgs);
|
276
|
+
},
|
277
|
+
|
278
|
+
// ActiveModel::Validations::ExclusionValidator
|
279
|
+
exclusion: function(options, messages) {
|
280
|
+
var stringIn = _(options['in']).map(function(o) {
|
281
|
+
return o.toString();
|
282
|
+
});
|
283
|
+
return closed(
|
284
|
+
_.include(stringIn, this.value) ? [messages.exclusion] : []
|
285
|
+
);
|
286
|
+
},
|
287
|
+
|
288
|
+
// ActiveModel::Validations::InclusionValidator
|
289
|
+
inclusion: function(options, messages) {
|
290
|
+
var stringIn = _(options['in']).map(function(o) {
|
291
|
+
return o.toString();
|
292
|
+
});
|
293
|
+
return closed(
|
294
|
+
!_.include(stringIn, this.value) ? [messages.inclusion] : []
|
295
|
+
);
|
296
|
+
},
|
297
|
+
|
298
|
+
// ActiveModel::Validations::NumericalityValidator
|
299
|
+
numericality: function(options, messages) {
|
300
|
+
var operators = {
|
301
|
+
greater_than: '>',
|
302
|
+
greater_than_or_equal_to: '>=',
|
303
|
+
equal_to: '==',
|
304
|
+
less_than: '<',
|
305
|
+
less_than_or_equal_to: '<='
|
306
|
+
},
|
307
|
+
msgs = [],
|
308
|
+
parsedValue = parseFloat(this.value, 10);
|
309
|
+
|
310
|
+
if (isNaN(Number(this.value))) {
|
311
|
+
msgs.push(messages.not_a_number);
|
312
|
+
} else {
|
313
|
+
if (options.odd && isEven(parsedValue)) msgs.push(messages.odd);
|
314
|
+
if (options.even && isOdd(parsedValue)) msgs.push(messages.even);
|
315
|
+
if (options.only_integer && !isInt(parsedValue)) msgs.push(messages.not_an_integer);
|
316
|
+
_(operators).each(function(operator, key) {
|
317
|
+
var valid = operate(parsedValue, operators[key], parseFloat(options[key], 10));
|
318
|
+
if (_(options).has(key) && !valid) {
|
319
|
+
msgs.push(messages[key]);
|
320
|
+
}
|
321
|
+
});
|
322
|
+
}
|
323
|
+
return closed(msgs);
|
324
|
+
},
|
325
|
+
|
326
|
+
// ActiveModel::Validations::FormatValidator
|
327
|
+
format: function(options, messages) {
|
328
|
+
var msgs = [];
|
329
|
+
if (_(options).has('with')) {
|
330
|
+
var withReg = convertRegExp(options['with']);
|
331
|
+
if (!withReg.test(this.value)) {
|
332
|
+
msgs.push(messages.invalid);
|
333
|
+
}
|
334
|
+
}
|
335
|
+
if (_(options).has('without')) {
|
336
|
+
var withoutReg = convertRegExp(options.without);
|
337
|
+
if (withoutReg.test(this.value)) {
|
338
|
+
msgs.push(messages.invalid);
|
339
|
+
}
|
340
|
+
}
|
341
|
+
return closed(msgs);
|
342
|
+
},
|
343
|
+
|
344
|
+
// ActiveModel::Validations::AcceptanceValidator
|
345
|
+
acceptance: function(options, messages) {
|
346
|
+
return closed(this.checked === true ? [] : [messages.accepted]);
|
347
|
+
},
|
348
|
+
|
349
|
+
// ActiveModel::Validations::ConfirmationValidator
|
350
|
+
confirmation: function(options, messages) {
|
351
|
+
var id = this.getAttribute('id'),
|
352
|
+
confId = id + '_confirmation',
|
353
|
+
confElem = root.document.getElementById(confId);
|
354
|
+
return closed(
|
355
|
+
this.value === confElem.value ? [] : [messages.confirmation]
|
356
|
+
);
|
357
|
+
},
|
358
|
+
|
359
|
+
// ActiveModel::Validations::UniquenessValidator
|
360
|
+
uniqueness: function(options, messages) {
|
361
|
+
var validation = pending();
|
362
|
+
get(urlFor(this, 'uniqueness'), {
|
363
|
+
success: function(status, headers, text) {
|
364
|
+
validation.close(text);
|
365
|
+
},
|
366
|
+
error: function(status, headers, text) {
|
367
|
+
validation.close(['Request error: ' + status]);
|
368
|
+
}
|
369
|
+
});
|
370
|
+
return validation;
|
371
|
+
}
|
372
|
+
};
|
373
|
+
|
374
|
+
var isCallbacksObj = function(obj) {
|
375
|
+
return _.isObject(obj) && _.has(obj, 'valid') && _.has(obj, 'invalid');
|
376
|
+
};
|
377
|
+
|
378
|
+
// Method for validating a form element. Pass either a single
|
379
|
+
// callback or one for valid and one for invalid.
|
380
|
+
judge.validate = function(element, callbacks) {
|
381
|
+
var queue = new ValidationQueue(element);
|
382
|
+
if (_.isFunction(callbacks)) {
|
383
|
+
queue.on('close', callbacks);
|
384
|
+
} else if (isCallbacksObj(callbacks)) {
|
385
|
+
queue.on('valid', callbacks.valid);
|
386
|
+
queue.on('invalid', callbacks.invalid);
|
387
|
+
}
|
388
|
+
return queue;
|
389
|
+
};
|
390
|
+
|
391
|
+
}).call(this);
|