valibot 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +250 -0
  3. data/js/valibot.js +503 -0
  4. data/lib/valibot.rb +117 -0
  5. metadata +90 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Kenichi Nakamura
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,250 @@
1
+ Valibot!
2
+ ========
3
+
4
+ Automatic field validation for forms backed by DataMapper models through Sinatra.
5
+
6
+ What it requires:
7
+ -----------------
8
+
9
+ * [Datamapper](http://datamapper.org/)
10
+ * [Sinatra](http://sinatrarb.com/)
11
+ * [Rack](http://rack.rubyforge.org/)
12
+ * [jQuery](http://jquery.com/)
13
+
14
+ What it provides:
15
+ -----------------
16
+
17
+ * full automatic field validation on default bind events
18
+ * field value matching confirmation
19
+ * extensible through callbacks used to build, place, remove, or transform error tags
20
+ * Valibot#all method to do the whole thing at once.
21
+ * automatic 'onsubmit' binding
22
+
23
+ To use (brief):
24
+ -------
25
+
26
+ * add this gem to your Gemfile, bundle install
27
+ * add the javascript to your site
28
+ * add the app to your config.ru (defaults to '/_valibot')
29
+ * create Valibots for the models on forms where you need them
30
+
31
+ To use (details):
32
+ -----------------
33
+
34
+ ####to add the javascript to your site, there are two methods:
35
+
36
+ Valibot has a route at '/valibot.js' that will deliver the source. This is the *preferred* method:
37
+
38
+ <script src="/_valibot/valibot.js"></script>
39
+
40
+ Valibot *can* add a helper to Sinatra::Base called 'valibot_js'. call this in a script template like so:
41
+
42
+ <%= valibot_js %>
43
+
44
+ They deliver the exact same content, but the route adds Cache-Control headers so layers like Varnish or Rack::Cache can do their work. In order to use the helper, you need to register it in your app:
45
+
46
+ class App < Sinatra::Base
47
+ register Valibot::Helpers
48
+ get('/'){ erb "<script><%= valibot_js %></script>" }
49
+ end
50
+
51
+ ####the sinatra app needs a place to run at in your Rack config. something like the following should suffice:
52
+
53
+ map '/_valibot' do
54
+ run Valibot::App
55
+ end
56
+
57
+ note that you can change the path it runs at, but you must pass that path as a value to the Valibot javascript constructor using the key `pathPrefix` (see below).
58
+
59
+ ####to create the Valibots, call the constructor inside $(document).ready():
60
+
61
+ $(document).ready(function(){ new Valibot({ modelName: 'user' }); });
62
+
63
+ Constructor options:
64
+ --------------------
65
+
66
+ ####modelName : string *required if no `modelNames`*
67
+ name of the model to validate against, in snake_case. in modern Rack-based webapps, the convention for form field names is to follow a `model_name[field_name]` pattern. Rack translates these names into the params hash and it is very common for the controller to pass this hash to a model constructor like `ModelName.new params['model_name']`. your form should follow this convention for Valibot to work, as it will use jQuery to select all `input`, `textarea`, and `select` tags from the DOM that have names that start with this value. ex:
68
+
69
+ <input type="text" name="user[first_name]"/>
70
+ <input type="text" name="user[email]"/>
71
+ ...
72
+ new Valibot({ modelName: 'user' });
73
+
74
+ ---
75
+
76
+ ####modelNames : array of strings *required if no `modelName`*
77
+ names of the models to validate against, in snake_case. this behaves exactly like `modelName` but allows multiple models to be validated in the same form. ex:
78
+
79
+ <input type="text" name="user[first_name]"/>
80
+ <input type="text" name="profile[bio]"/>
81
+ ...
82
+ new Valibot({ modelNames: ['user', 'profile'] });
83
+
84
+ note that the first invalid field of the _first model specified_ will be `focus()`ed if the form fails validation on submit.
85
+
86
+ ---
87
+
88
+ ####pathPrefix : string
89
+ path you have Valibot racked up at. ex:
90
+
91
+ new Valibot({ modelName: 'user', pathPrefix: 'dogs_and_cats' });
92
+
93
+ ---
94
+
95
+ ####errorTagBuilder : function(text, self, minion)
96
+ a callback for building the tag you want to put in the DOM and show the user with the error message from DataMapper in it. the default builder will create an `<em>` element, set its class to 'error', innerHTML `text` in, and return it.
97
+ ######parameters:
98
+ * `text` - error message from DataMapper
99
+ * `self` - reference to the Valibot object
100
+ * `minion` - reference to the Valitron or Confirm-o-mat object
101
+ ######returns:
102
+ * an element with the `text` in it suitable for placing in to the DOM.
103
+
104
+ ---
105
+
106
+ ####errorTagPlacer : function(tag, errorTag, self, minion)
107
+ a callback for placing the element returned from the builder into the DOM.
108
+ ######parameters:
109
+ * `tag` - the field element whose value is being validated
110
+ * `errorTag` - the element built and returned by the `errorTagBuilder` function of this Valibot
111
+ * `self` - reference to the Valibot object
112
+ * `minion` - reference to the Valitron or Confirm-o-mat object
113
+
114
+ ---
115
+
116
+ ####errorTagRemover : function(errorTag, self, minion)
117
+ a callback for removing the element from the DOM in the event of no error found.
118
+ ######parameters:
119
+ * `errorTag` - the element in the DOM that holds the error message for this field
120
+ * `self` - reference to the Valibot object
121
+ * `minion` - reference to the Valitron or Confirm-o-mat object
122
+
123
+ ---
124
+
125
+ ####errorTagTransformer : function(text, errorTag, self, minion)
126
+ a callback for transforming the element in the DOM in the event of a new error message replacing an old one.
127
+ ######parameters:
128
+ * `text` - error message from DataMapper
129
+ * `errorTag` - the element in the DOM that holds the error message for this field
130
+ * `self` - reference to the Valibot object
131
+ * `minion` - reference to the Valitron or Confirm-o-mat object
132
+
133
+ ---
134
+
135
+ ####include : array or map { modelName : array }
136
+ by default, Valibot will attach Valitrons to elements whose name attribute matches a specific regular expression, after jQuery selection. you can force fields whose name attributes don't match the regex to be checked by Valibot by putting the field name in this array. i'm not actually sure when this would be needed. :)
137
+
138
+ ---
139
+
140
+ ####exclude : array or map { modelName : array }
141
+ you can keep Valibot from attaching Valitrons to certain elements by putting their field names in this
142
+ array. ex:
143
+
144
+ new Valibot({ modelName: 'user', exclude: ['middle_name'] });
145
+
146
+ new Valibot({ modelNames: ['user', 'profile'], exclude: {'user': ['middle_name'], 'profile': ['icon']});
147
+
148
+ ---
149
+
150
+ ####bindEvents : map { fieldName : event }
151
+ Valibot has a fairly intuitive default bind event for each input type: `change` for checkboxes, radio buttons, and select drop-downs; `blur` for everything else. you can override these defaults on a per-field basis by passing them in this hash. ex:
152
+
153
+ new Valibot({ modelName: 'user', bindEvents: {'middle_name':'keyup'} });
154
+
155
+ ---
156
+
157
+ ####context : string
158
+ DataMapper has contextual validations built-in. if you need to validate against a certain context, pass it in here. ex:
159
+
160
+ new Valibot({ modelName: 'user', context: 'signup' });
161
+
162
+ ---
163
+
164
+ ####confirm : array of maps { id : things }
165
+ often on a form, one will have an 'extra' field that is not part of the data model, but needs to match another field in value. usually, these are passwords or email addresses. in this case, if you have given the actual model field element an id of 'password', the confirmation field id should be 'confirm_password'. then you can tell Valibot about this by passing this array of hashes and it will attach a Confirm-o-mat to each. `things` is the word that the Confirm-o-mat will use in its error message about the things not matching. ex:
166
+
167
+ new Valibot({ modelName: 'user', confirm: [{'password', 'Passwords'}] });
168
+
169
+ this would attach a Confirm-o-mat to `$('#confirm_password')` that checks its value against `$('#password').val()` and will build an errorTag with the text, "Passwords do not match."
170
+
171
+ ---
172
+
173
+ ####agreements : array of maps { id : things }
174
+ often on a form, one will have a checkbox that is not part of the data model, but needs to checked as a legally binding agreement. usually, these are rules or term and conditions, etc. in this case, simply give the input a unique id. then you can tell Valibot about it by passing this array of hashes and it will attach an Agree-Pee-Oh to each. `things` is the word that the Agree-Pee-Oh will use in its error message about the things not being agreed to. ex:
175
+
176
+ new Valibot({ modelName: 'user', agreements: [{'rules', 'Rules'}] });
177
+
178
+ this would attach an Agree-Pee-Oh to `$('#rules')` that verified it's checked and will build an errorTag with the text, "You must agree to the Rules." if it is not.
179
+
180
+ ---
181
+
182
+ ####fancy : boolean
183
+ Valibot has two built-in types of errorTag(Placer|Remover|Transformer) functions. the default is very bland and simply inserts, replaces, or removes the errorTag element. the fancy option uses jQuery animation to fadeIn and fadeOut the errorTag element. if this is `true`, and you also feed it your own errorTag callbacks, those will be used instead. ex:
184
+
185
+ new Valibot({ modelName: 'user', fancy: true, errorTagPlacer: myETPlacer });
186
+
187
+ this would utilize the "fancy" Remover and Transformer built-in functions, but call `myETPlacer()` for placement. note that the `_fancyErrorTagTransformer` function actually calls whatever Placer function is set in the containing Valibot; in this case, `myETPlacer`.
188
+
189
+ ---
190
+
191
+ ####bindOnSubmit : boolean
192
+ Valibot automatically binds it's own `onSubmit()` function to the first form that it finds model fields in. this function will run through any unchecked or invalid 'trons or 'mats and call their `checkValue()` functions. once that is complete, if all of them are valid, it submits the form; otherwise, error messages are built/placed like normal, and the first invalid field is focus()ed. this flag allows you to turn off this functionality, for cases where you need to do your own onsubmit dance. note that the Valibot's onsubmit can be called in this situation anyway; but, be careful because it will submit the form if everything is valid. ex:
193
+
194
+ var valibot = new Valibot({ modelName: 'user', bindOnSubmit: false });
195
+ $(valibot.form).bind('submit', function(event) {
196
+ event.preventDefault();
197
+
198
+ // do something clever
199
+
200
+ valibot.onSubmit();
201
+ });
202
+
203
+ ---
204
+
205
+ ####submitForm : boolean
206
+ sometimes, you just want to submit the form yourself. most of the time, you want to do it all AJAX fancypants. fine, just pass this in as `false` and Valibot won't try to submit the form. it will still call `onSubmitCallback` so you probably want to put your XHRs in there.
207
+
208
+ ---
209
+
210
+ ####onSubmitCallback : function(valibot)
211
+ set this to a function you want to run when the form would be submitted. if you set `submitForm` to false, this function will be called, but the form WILL NOT be submitted. use these two together to get Valibot validation on forms you want to submit XHR-style. ex:
212
+
213
+ var valibot = new Valibot({ modelName: 'foo', submitForm: false, onSubmitCallback: function(valibot) {
214
+ $.post(valibot.form.action, $(valibot.form).serialize(), function(response){ /* ... /* });
215
+ }});
216
+
217
+ ---
218
+
219
+ ####appUrl : string
220
+ perhaps you want to run Valibot at a different URL completely from the one that the form your validating loaded from. fine, just tell Valibot about it with this. note that this puts Valibot into JSONP mode and all validation requests turn from POSTs into GETs. ex:
221
+
222
+ new Valibot({ modelName: 'user', appUrl: 'http://some.other.domain/'});
223
+
224
+ this would cause all validation requests to go to 'http://some.other.domain/_valibot/:model/...'
225
+
226
+ ---
227
+
228
+ ####dependents : map of arrays { field : [ otherField, ... ] }
229
+ sometimes you need to validate a field based off of the value of another field. you can tell Valibot about this with this option, and it will automatically include the value(s) of the other field(s) in the validation. ex:
230
+
231
+ new Valibot({ modelName: 'user', dependents: {'last_name': ['first_name']} });
232
+
233
+ this would cause Valibot to POST the value of the field named 'user[first_name]' with the check request for 'last_name'. your model should be configured to validate `last_name` based off of the value of `first_name`:
234
+
235
+ validates_with_block :last_name do
236
+ if self.first_name == 'a' and self.last_name == 'b'
237
+ [false, "can't have b with a."]
238
+ else
239
+ true
240
+ end
241
+ end
242
+
243
+ note that in the above example, `first_name` would still be validated on it's own.
244
+
245
+ Next Steps (TODOS):
246
+ -------------------
247
+
248
+ * automated testing (!!)
249
+ * array parameters
250
+ * profit!
data/js/valibot.js ADDED
@@ -0,0 +1,503 @@
1
+ /* valibot.js (c) 2011 kenichi nakamura (kenichi.nakamura@gmail.com)
2
+ *
3
+ * javascript class for easy automatic form field validation against datamapper
4
+ * models behind a sinatra app.
5
+ *
6
+ * see ____ for details. TODO
7
+ */
8
+
9
+ // regex to make sure we only grab fields whose 'name' attrs match
10
+ var _valibotFieldRegExp = new RegExp('^\\w+\\[[^\\]]+\\]$');
11
+
12
+ if (Array.prototype.indexOf == null) {
13
+ Array.prototype.indexOf = function(obj) {
14
+ for (var i = 0; i < this.length; i++) {
15
+ if (this[i] == obj) {
16
+ return i;
17
+ }
18
+ }
19
+ return -1;
20
+ };
21
+ }
22
+ Array.prototype.contains = function(obj){ return this.indexOf(obj) != -1; };
23
+
24
+ /* the Valibot class. create an object of this class and feed it a model name. oh,
25
+ * and options too if you want. the object, during construction, will attach
26
+ * Valitrons (see below) to each field it finds that matches the modelName specified.
27
+ * it will also attach Confirmomats (see below) to any field specified as a
28
+ * confirmation field.
29
+ */
30
+ function Valibot(opts) {
31
+
32
+ this.pathPrefix = '_valibot';
33
+ this.modelNames = [];
34
+ this.errorTagPlacer = this._defaultErrorTagPlacer;
35
+ this.errorTagBuilder = this._defaultErrorTagBuilder;
36
+ this.errorTagRemover = this._defaultErrorTagRemover;
37
+ this.errorTagTransformer = this._defaultErrorTagTransformer;
38
+ this.include = {};
39
+ this.exclude = {};
40
+ this.bindEvents = {};
41
+ this.context = null;
42
+ this.confirm = [];
43
+ this.confirmomats = [];
44
+ this.valitrons = [];
45
+ this.valids = [];
46
+ this.invalids = [];
47
+ this.bindOnSubmit = true;
48
+ this.submitForm = true;
49
+ this.onSubmitCallback = null;
50
+ this.agreements = [];
51
+ this.agreepeeohs = [];
52
+ this.formId = '';
53
+ this.appUrl = null;
54
+ this.dependents = {};
55
+
56
+ if (opts != null) {
57
+ if (opts.fancy != null && opts.fancy) {
58
+ this.errorTagPlacer = this._fancyErrorTagPlacer;
59
+ this.errorTagRemover = this._fancyErrorTagRemover;
60
+ this.errorTagTransformer = this._fancyErrorTagTransformer;
61
+ }
62
+ if (opts.modelName != null) this.modelNames = [opts.modelName];
63
+ if (opts.modelNames != null) this.modelNames = opts.modelNames;
64
+ if (opts.pathPrefix != null) this.pathPrefix = opts.pathPrefix;
65
+ if (opts.errorTagPlacer != null) this.errorTagPlacer = opts.errorTagPlacer;
66
+ if (opts.errorTagBuilder != null) this.errorTagBuilder = opts.errorTagBuilder;
67
+ if (opts.errorTagRemover != null) this.errorTagRemover = opts.errorTagRemover;
68
+ if (opts.errorTagTransformer != null) this.errorTagTransformer = opts.errorTagTransformer;
69
+ if (opts.bindEvents != null) this.bindEvents = opts.bindEvents;
70
+ if (opts.context != null) this.context = opts.context;
71
+ if (opts.confirm != null) this.confirm = opts.confirm;
72
+ if (opts.bindOnSubmit != null) this.bindOnSubmit = opts.bindOnSubmit;
73
+ if (opts.submitForm != null) this.submitForm = opts.submitForm;
74
+ if (opts.onSubmitCallback != null) this.onSubmitCallback = opts.onSubmitCallback;
75
+ if (opts.agreements != null) this.agreements = opts.agreements;
76
+ if (opts.formId != null) this.formId = '#' + opts.formId + ' ';
77
+ if (opts.appUrl != null) this.appUrl = opts.appUrl;
78
+ if (opts.dependents != null) this.dependents = opts.dependents;
79
+
80
+ if (opts.include != null) {
81
+ if (opts.include instanceof Array) {
82
+ this.include[this.modelNames[0]] = opts.include;
83
+ } else {
84
+ this.include = opts.include;
85
+ }
86
+ }
87
+ if (opts.exclude != null) {
88
+ if (opts.exclude instanceof Array) {
89
+ this.exclude[this.modelNames[0]] = opts.exclude;
90
+ } else {
91
+ this.exclude = opts.exclude;
92
+ }
93
+ }
94
+
95
+ }
96
+
97
+ var _this = this;
98
+ $(this.modelNames).each(function(i, modelName) {
99
+ $(_this.formId + 'input[name^=' + modelName + '], ' +
100
+ _this.formId + 'select[name^=' + modelName + '], ' +
101
+ _this.formId + 'textarea[name^=' + modelName + ']').each(function(i,f) {
102
+
103
+ var _name = $(f).attr('name');
104
+ var field = _name.substring(_name.indexOf('[') + 1, _name.length - 1);
105
+
106
+ var _type = f.type.toLowerCase();
107
+ var _defaultBindEvent = 'blur';
108
+ if (_type == 'checkbox' || _type == 'radio' || _type == 'select-one' || _type == 'select-multiple')
109
+ _defaultBindEvent = 'change';
110
+
111
+ var bindEvent = (_this.bindEvents[field] != null) ? _this.bindEvents[field] : _defaultBindEvent;
112
+
113
+ if (_this.include[modelName] instanceof Array && _this.include[modelName].indexOf(field) != -1) {
114
+ _this.valitrons.push(new Valitron(f, modelName, field, bindEvent, _this));
115
+ } else if (_this.exclude[modelName] instanceof Array && _this.exclude[modelName].indexOf(field) != -1) {
116
+ // NOOP
117
+ } else {
118
+ if (_valibotFieldRegExp.test(f.name))
119
+ _this.valitrons.push(new Valitron(f, modelName, field, bindEvent, _this));
120
+ }
121
+
122
+ });
123
+ });
124
+
125
+ if (this.confirm != null && this.confirm.length > 0) {
126
+ $(this.confirm).each(function(i,c) {
127
+ $.each(c, function(id, things) { _this.confirmomats.push(new Confirmomat(id, things, _this)); });
128
+ });
129
+ }
130
+
131
+ if (this.agreements != null && this.agreements.length > 0) {
132
+ $(this.agreements).each(function(i,c) {
133
+ $.each(c, function(id, things) { _this.agreepeeohs.push(new AgreePeeOh(id, things, _this)); });
134
+ });
135
+ }
136
+
137
+ this.form = this.valitrons.concat(this.confirmomats).concat(this.agreepeeohs)[0].tag.form;
138
+ if (this.bindOnSubmit)
139
+ $(this.form).submit(function(e){ e.preventDefault(); _this.onSubmit(); });
140
+
141
+ }
142
+
143
+ Valibot.prototype = {
144
+
145
+ addValid:
146
+ function(good) {
147
+ if (!this.valids.contains(good)) this.valids.push(good);
148
+ if (this.invalids.contains(good)) this.invalids.splice(this.invalids.indexOf(good), 1);
149
+ },
150
+
151
+ removeValid:
152
+ function(remove) {
153
+ if (this.valids.contains(remove)) this.valids.splice(this.valids.indexOf(remove), 1);
154
+ },
155
+
156
+ addInvalid:
157
+ function(bad) {
158
+ if (!this.invalids.contains(bad)) this.invalids.push(bad);
159
+ this.removeValid(bad);
160
+ },
161
+
162
+ unchecked:
163
+ function() {
164
+ var uc = [];
165
+ var _this = this;
166
+ $(this.valitrons).each(function(i,tron){ if (!_this.valids.contains(tron) && !_this.invalids.contains(tron)) uc.push(tron); });
167
+ $(this.confirmomats).each(function(i,mat){ if (!_this.valids.contains(mat) && !_this.invalids.contains(mat)) uc.push(mat); });
168
+ $(this.agreepeeohs).each(function(i,oh){ if (!_this.valids.contains(oh) && !_this.invalids.contains(oh)) uc.push(oh); });
169
+ return uc;
170
+ },
171
+
172
+ allValid:
173
+ function() {
174
+ return this.invalids.length == 0 &&
175
+ this.valids.length == this.valitrons.length + this.confirmomats.length + this.agreepeeohs.length;
176
+ },
177
+
178
+ onSubmit:
179
+ function() {
180
+ if (!this.allValid()) {
181
+ this.checking = this.invalids.concat(this.unchecked());
182
+ var _this = this;
183
+ $(this.invalids).each(function(i, bad){ bad.checkValue(null, function(valid) { _this.onSubmitCheckCallback(bad); }); });
184
+ $(this.unchecked()).each(function(i, uc){ uc.checkValue(null, function(valid) { _this.onSubmitCheckCallback(uc); }); });
185
+ } else {
186
+ if (this.submitForm) {
187
+ this.form.submit();
188
+ } else if (typeof this.onSubmitCallback === 'function') {
189
+ this.onSubmitCallback(this);
190
+ }
191
+ }
192
+ },
193
+
194
+ onSubmitCheckCallback:
195
+ function(tronOrMat, valid) {
196
+ this.checking.splice(this.checking.indexOf(tronOrMat), 1);
197
+ if (this.checking.length == 0) {
198
+ if (!this.allValid()) {
199
+ this.invalids[0].tag.focus();
200
+ } else {
201
+ if (this.submitForm) {
202
+ this.form.submit();
203
+ } else if (typeof this.onSubmitCallback === 'function') {
204
+ this.onSubmitCallback(this);
205
+ }
206
+ }
207
+ }
208
+ },
209
+
210
+ // never called by Valibot or its minions, but left here for utility.
211
+ all:
212
+ function(callback) {
213
+ var params = {};
214
+ $(this.valitrons).each(function(i, tron){ params[tron.tag.name] = tron.tag.value; });
215
+ var path = (this.appUrl != null ? this.appUrl : '') +
216
+ '/' + this.pathPrefix +
217
+ '/' + this.modelName +
218
+ (this.context != null ? ('/in/' + this.context) : '');
219
+ if (this.context != null)
220
+ path = path + '/in/' + this.context;
221
+ $.post(path, params, function(res){ callback(res) },
222
+ this.valibot.appUrl != null ? 'jsonp' : 'json');
223
+ },
224
+
225
+ _defaultErrorTagPlacer:
226
+ function(tag, errorTag, self, minion) {
227
+ $(errorTag).insertAfter($(tag));
228
+ },
229
+
230
+ _defaultErrorTagBuilder:
231
+ function(text, self, minion) {
232
+ var em = document.createElement('em');
233
+ em.className = 'error';
234
+ em.innerHTML = text;
235
+ return em;
236
+ },
237
+
238
+ _defaultErrorTagRemover:
239
+ function(errorTag, self, callback, minion) {
240
+ var ret = $(errorTag).remove();
241
+ if (callback != null) callback(ret);
242
+ },
243
+
244
+ _defaultErrorTagTransformer:
245
+ function(text, errorTag, self, minion) {
246
+ var em = this.errorTagBuilder(text, self, minion);
247
+ $(errorTag).replaceWith(em);
248
+ return em;
249
+ },
250
+
251
+ _fancyErrorTagPlacer:
252
+ function(tag, errorTag, self, minion) {
253
+ $(errorTag).css('display', 'none');
254
+ $(errorTag).insertAfter($(tag));
255
+ $(errorTag).fadeIn();
256
+ },
257
+
258
+ _fancyErrorTagRemover:
259
+ function(errorTag, self, minion) {
260
+ $(errorTag).fadeOut(null, null, function(){ $(errorTag).remove(); });
261
+ },
262
+
263
+ _fancyErrorTagTransformer:
264
+ function(text, errorTag, self, minion) {
265
+ if (text == errorTag.innerHTML) {
266
+ return errorTag;
267
+ } else {
268
+ var newErrorTag = self.errorTagBuilder(text, self, minion);
269
+ $(errorTag).fadeOut(null, null, function() {
270
+ var tag = $(errorTag).prev();
271
+ $(errorTag).remove();
272
+ self.errorTagPlacer(tag, newErrorTag);
273
+ });
274
+ return newErrorTag;
275
+ }
276
+ }
277
+
278
+ };
279
+
280
+ // ---
281
+
282
+ /* the Valitron class. instantiated on each model form field by an overseeing
283
+ * Valibot. handles the actual XHR between the JS and the valibot sinatra app.
284
+ */
285
+ function Valitron(tag, model, field, bindEvent, valibot) {
286
+ this.valid = false;
287
+ this.tag = tag;
288
+ this.model = model;
289
+ this.field = field;
290
+ this.valibot = valibot;
291
+ this.errorTag = null;
292
+ var _this = this;
293
+ $(this.tag).bind(bindEvent, function(event){ _this.checkValue(event); });
294
+ $(this.tag).bind('keyup', function(event){ _this.reset(); });
295
+ }
296
+
297
+ Valitron.prototype = {
298
+
299
+ checkValue:
300
+ function(event, callback) {
301
+ var _this = this;
302
+ var path = (this.valibot.appUrl != null ? this.valibot.appUrl : '') +
303
+ '/' + this.valibot.pathPrefix +
304
+ '/' + this.model +
305
+ '/' + this.field +
306
+ (this.valibot.context != null ? ('/in/' + this.valibot.context) : '');
307
+ var val = this.tag.value;
308
+ if (this.tag.type.toLowerCase() == 'checkbox') val = this.tag.checked;
309
+ var params = {value: val};
310
+ if (this.valibot.dependents[this.field]) {
311
+ $(this.valibot.dependents[this.field]).each(function(i,dep) {
312
+ params[_this.model + '[' + dep + ']'] =
313
+ $('input[name="' + _this.model + '[' + dep + ']"], ' +
314
+ 'select[name="' + _this.model + '[' + dep + ']"], ' +
315
+ 'textarea[name="' + _this.model + '[' + dep + ']"]').val();
316
+ });
317
+ }
318
+ $.post(path, params, function(res){ _this.handleResponse(res, callback); },
319
+ this.valibot.appUrl != null ? 'jsonp' : 'json');
320
+ },
321
+
322
+ handleResponse:
323
+ function(res, callback) {
324
+
325
+ // bad state -> good state
326
+ if (res == null && this.errorTag != null) {
327
+ var _this = this;
328
+ this.valibot.errorTagRemover(this.errorTag, this.valibot, function(ret){ _this.errorTag = null; }, this);
329
+ this.valibot.addValid(this);
330
+ if (callback != null) callback(true);
331
+ this.valid = true;
332
+
333
+ // null state -> good state
334
+ } else if (res == null && this.errorTag == null) {
335
+ this.valibot.addValid(this);
336
+ if (callback != null) callback(true);
337
+ this.valid = true;
338
+
339
+ // bad state -> bad state
340
+ } else if (res != null && res.error != null && this.errorTag != null) {
341
+ this.errorTag = this.valibot.errorTagTransformer(this.parseErrorStrings(res.error), this.errorTag, this.valibot, this);
342
+ this.valibot.addInvalid(this);
343
+ if (callback != null) callback(false);
344
+ this.valid = false;
345
+
346
+ // null state -> bad state
347
+ } else if (res != null && res.error != null && this.errorTag == null) {
348
+ this.errorTag = this.valibot.errorTagBuilder(this.parseErrorStrings(res.error), this.valibot, this);
349
+ this.valibot.errorTagPlacer(this.tag, this.errorTag, this.valibot, this);
350
+ this.valibot.addInvalid(this);
351
+ if (callback != null) callback(false);
352
+ this.valid = false;
353
+ }
354
+ },
355
+
356
+ parseErrorStrings:
357
+ function(error) {
358
+ var es = '';
359
+ $(error).each(function(i, e) { es += e + '<br/>' });
360
+ return es.substring(0, es.length - 5); // remove last <br/>
361
+ },
362
+
363
+ reset:
364
+ function() {
365
+ this.valibot.removeValid(this);
366
+ this.valid = false;
367
+ }
368
+
369
+ };
370
+
371
+ // ---
372
+
373
+ /* the Confirm-o-mat class. instantiated on form fields that are specified as
374
+ * needing to match another field by an overseeing Valibot. handles the test
375
+ * for equal values between fields.
376
+ */
377
+ function Confirmomat(id, things, valibot) {
378
+ if ($('#' + id).length == 1 && $('#confirm_' + id).length == 1) {
379
+ this.valid = false;
380
+ this.tag = $('#confirm_' + id)[0];
381
+ this.id = id;
382
+ this.things = things;
383
+ this.valibot = valibot;
384
+ this.errorTag = null;
385
+ var _this = this;
386
+ $(this.tag).bind('blur', function(event){ _this.checkValue(event); });
387
+ $(this.tag).bind('keyup', function(event){ _this.reset(); });
388
+ $('#' + this.id).bind('keyup', function(event){ _this.reset(); });
389
+ } else {
390
+ // console.log('ERROR: matching fields not found or too many found.');
391
+ return null;
392
+ }
393
+ }
394
+
395
+ Confirmomat.prototype = {
396
+
397
+ checkValue:
398
+ function(event, callback) {
399
+ if ($('#' + this.id).val() == $(this.tag).val()) {
400
+
401
+ // bad state -> good state
402
+ if (this.errorTag != null) {
403
+ var _this = this;
404
+ this.valibot.errorTagRemover(this.errorTag, this.valibot, function(ret){ _this.errorTag = null; }, this);
405
+ }
406
+
407
+ // * state -> good state
408
+ this.valibot.addValid(this);
409
+ if (callback != null) callback(true);
410
+ this.valid = true;
411
+
412
+ } else {
413
+
414
+ // bad state -> bad state
415
+ if (this.errorTag != null) {
416
+ this.valibot.errorTagTransformer(this.things + " do not match.", this.errorTag, this.valibot, this);
417
+
418
+ // null state -> bad state
419
+ } else {
420
+ this.errorTag = this.valibot.errorTagBuilder(this.things + " do not match.", this.valibot, this);
421
+ this.valibot.errorTagPlacer(this.tag, this.errorTag, this.valibot, this);
422
+ }
423
+
424
+ // * state -> bad state
425
+ this.valibot.addInvalid(this);
426
+ if (callback != null) callback(false);
427
+ this.valid = false;
428
+
429
+ }
430
+ },
431
+
432
+ reset:
433
+ function() {
434
+ // console.log('confirmomat resetting.');
435
+ this.valibot.removeValid(this);
436
+ this.valid = false;
437
+ }
438
+
439
+ };
440
+
441
+ // ---
442
+
443
+ /* the AgreePeeOh class. instantiated on checkboxes that are specified as
444
+ * needing to be checked for form submission to be allowed.
445
+ */
446
+ function AgreePeeOh(id, things, valibot) {
447
+ if ($('#' + id).length == 1) {
448
+ this.valid = false;
449
+ this.id = id;
450
+ this.tag = $('#' + id)[0];
451
+ this.things = things;
452
+ this.valibot = valibot;
453
+ this.errorTag = null;
454
+ var _this = this;
455
+ $(this.tag).bind('change', function(event){ _this.checkValue(event); });
456
+ } else {
457
+ return null;
458
+ }
459
+ }
460
+
461
+ AgreePeeOh.prototype = {
462
+
463
+ checkValue:
464
+ function(event, callback) {
465
+ if ($('#' + this.id).attr('checked')) {
466
+
467
+ // bad state -> good state
468
+ if (this.errorTag != null) {
469
+ var _this = this;
470
+ this.valibot.errorTagRemover(this.errorTag, this.valibot, function(ret){ _this.errorTag = null; }, this);
471
+ }
472
+
473
+ // * state -> good state
474
+ this.valibot.addValid(this);
475
+ if (callback != null) callback(true);
476
+ this.valid = true;
477
+
478
+ } else {
479
+
480
+ // bad state -> bad state
481
+ if (this.errorTag != null) {
482
+ this.valibot.errorTagTransformer("You must agree to the " + this.things + ".", this.errorTag, this.valibot, this);
483
+
484
+ // null state -> bad state
485
+ } else {
486
+ this.errorTag = this.valibot.errorTagBuilder("You must agree to the " + this.things + ".", this.valibot, this);
487
+ this.valibot.errorTagPlacer(this.tag, this.errorTag, this.valibot, this);
488
+ }
489
+
490
+ // * state -> bad state
491
+ this.valibot.addInvalid(this);
492
+ if (callback != null) callback(false);
493
+ this.valid = false;
494
+
495
+ }
496
+ },
497
+
498
+ reset:
499
+ function() {
500
+ this.valibot.removeValid(this);
501
+ this.valid = false;
502
+ }
503
+ };
data/lib/valibot.rb ADDED
@@ -0,0 +1,117 @@
1
+ # valibot.rb (c) 2011 kenichi nakamura (kenichi.nakamura@gmail.com)
2
+ #
3
+ # easy automatic form field validation against datamapper models behind a sinatra app.
4
+ # this is the helper module and Sinatra app class.
5
+ #
6
+ # see ____ for details. TODO
7
+
8
+ module Valibot
9
+
10
+ JAVASCRIPT = File.join File.dirname(__FILE__), '..', 'js', 'valibot.js'
11
+
12
+ module Helpers
13
+
14
+ def self.registered app; app.helpers Valibot::Helpers; end
15
+ def valibot_js; File.read Valibot::JAVASCRIPT; end
16
+
17
+ end
18
+
19
+ class App < Sinatra::Base
20
+
21
+ before do
22
+ set_content_type
23
+ end
24
+
25
+ get '/valibot.js' do
26
+ content_type :js
27
+ cache_control :public, :max_age => 10 * 60
28
+ File.read Valibot::JAVASCRIPT
29
+ end
30
+
31
+ post '/:model/:field/in/:context' do
32
+ validate_field_value *parse(params)
33
+ end
34
+
35
+ post '/:model/in/:context' do
36
+ validate_model *parse(params)
37
+ end
38
+
39
+ post '/:model/:field' do
40
+ validate_field_value *parse(params)
41
+ end
42
+
43
+ post '/:model' do
44
+ validate_model *parse(params)
45
+ end
46
+
47
+ get '/:model/:field/in/:context' do
48
+ jsonp validate_field_value *parse(params)
49
+ end
50
+
51
+ get '/:model/in/:context' do
52
+ jsonp validate_model *parse(params)
53
+ end
54
+
55
+ get '/:model/:field' do
56
+ jsonp validate_field_value *parse(params)
57
+ end
58
+
59
+ get '/:model' do
60
+ jsonp validate_model *parse(params)
61
+ end
62
+
63
+ private
64
+
65
+ def parse params = {}
66
+ model_class_name = params[:model].camel_case
67
+ model_class = begin
68
+ Kernel.const_get(model_class_name)
69
+ rescue NameError => e
70
+ halt(Yajl.dump :error => e.message)
71
+ end
72
+ field_sym = params[:field] ? params[:field].to_sym : nil
73
+ context = params[:context] ? params[:context].to_sym : :default
74
+ dependents = params[params[:model]]
75
+ [model_class, field_sym, context, dependents].compact
76
+ end
77
+
78
+ def validate_field_value model = nil, field = nil, context = :default, dependents = {}
79
+ if model && model.included_modules.include?(DataMapper::Resource)
80
+ begin
81
+ if model.properties.map(&:name).include?(field) or model.new.__send__(field).kind_of?(DataMapper::Collection)
82
+ obj = model.new dependents.merge field => params['value']
83
+ Yajl.dump :error => obj.errors[field] if !obj.valid?(context) && obj.errors.keys.include?(field)
84
+ else
85
+ Yajl.dump :error => "Invalid field: #{params[:field]}"
86
+ end
87
+ rescue NoMethodError => e
88
+ Yajl.dump :error => "Invalid field: #{params[:field]}"
89
+ end
90
+ else
91
+ Yajl.dump :error => "Invalid model: #{params[:model].camel_case}"
92
+ end
93
+ end
94
+
95
+ def validate_model model = nil, context = :default
96
+ if model && model.included_modules.include?(DataMapper::Resource)
97
+ obj = model.new params[params[:model]]
98
+ unless obj.valid?(context)
99
+ Yajl.dump :error => obj.errors.to_hash
100
+ end
101
+ else
102
+ Yajl.dump :error => "Invalid model: #{params[:model].camel_case}"
103
+ end
104
+ end
105
+
106
+ def set_content_type
107
+ content_type (params['callback'] ? :js : :json)
108
+ end
109
+
110
+ def jsonp json = ''
111
+ return json unless params['callback']
112
+ params['callback'] + "(#{json})"
113
+ end
114
+
115
+ end
116
+
117
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: valibot
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.5
6
+ platform: ruby
7
+ authors:
8
+ - Kenichi Nakamura
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-08-02 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.0
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: dm-validations
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 1.0.0
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: sinatra
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 1.1.0
46
+ type: :runtime
47
+ version_requirements: *id003
48
+ description:
49
+ email:
50
+ - kenichi.nakamura@gmail.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files: []
56
+
57
+ files:
58
+ - lib/valibot.rb
59
+ - js/valibot.js
60
+ - LICENSE
61
+ - README.md
62
+ homepage: https://github.com/kenichi/valibot
63
+ licenses: []
64
+
65
+ post_install_message:
66
+ rdoc_options: []
67
+
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.3.4
82
+ requirements: []
83
+
84
+ rubyforge_project: valibot
85
+ rubygems_version: 1.8.6
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Automatic field validation for forms backed by DataMapper models through Sinatra.
89
+ test_files: []
90
+