├── README.md
├── Source
└── ValidateSimple.js
└── package.yml
/README.md:
--------------------------------------------------------------------------------
1 | ValidateSimple
2 | ==============
3 |
4 | A MooTools class for realtime, client-side validation of form inputs. Given a form it will
5 | fire events as inputs become valid/invalid as the user is typing. It will also, of course,
6 | alert you when the entire form is valid.
7 |
8 | Validators themselves can be created by you, and a small set of them is provided to get you
9 | started (in the ValidateSimple.Validators object). Validators can optionally be asynchronous, which is useful when using some server-side validation logic.
10 |
11 | _Note: this is not a drop-in "plugin" that will do everything for you. This code does not
12 | enforce any kind of UI whatsoever, it just fires events and lets you handle what is
13 | displayed. If you want a more hands-off validator use the standard MooTools More
14 | one. I created this to be more light-weight, have greater flexibility in when forms are
15 | validated and handle asynchronous validations._
16 |
17 | 
18 |
19 |
20 |
21 | How to use
22 | ----------
23 |
24 | In your HTML, add classnames of the form "validate-TYPE" where TYPE is something like "email".
25 |
26 |
27 |
28 | In your Moo code, do something like:
29 |
30 | new ValidateSimple(form_element, {
31 | inputs: 'form input[type=text], form input[type=email]',
32 | onValid: function(){
33 | // do something like activate the submit button
34 | },
35 | onInvalid: function(){
36 | // do something like deactivate the submit button
37 | },
38 | onInputInvalid: function(input, errors){
39 | // do something like show an error message somewhere, possibly based off the input's title attr
40 | // errors (2nd arg) will contain an array of strings - each string is the validation type that
41 | // failed. Example: ['email', 'no-troll']
42 | }
43 | });
44 |
45 | Here's what happens next:
46 |
47 | 1. The user starts typing into one of the inputs.
48 | 2. The user tabs/clicks out of the input (possibly to another).
49 | 3. The user made a mistake, and onInputInvalid is fired. A class "invalid" is automatically
50 | added to the input as well.
51 | 4. The user clicks back to the input to make a mistake.
52 | 5. Now, as they type, each keyup will check the input and remove the "invalid" class as soon
53 | as it is correct. The onInputInvalid and onInputValid events will also fire for each keyup.
54 | 6. Once all inputs are valid (meaning they don't have the "invalid" class), the
55 | onValid event fires.
56 |
57 | What this means is that the first time a user fills out a given input, it will
58 | wait until they are finished to alert them. But, once they go back to an input
59 | alerting (of valid _or_ invalid) will happen as they type. The entire form's
60 | validity is always based on each keyup.
61 |
62 |
63 | Custom Validators
64 | -----------------
65 |
66 | Let's say you did this in your html
67 |
68 |
69 |
70 | You could create this validator anywhere in your Javascript:
71 |
72 | ValidateSimple.Validators['no-troll'] = {
73 | test: function(input){
74 | return !input.get('value').test(/<\s*script/i) && !input.get('value').test(/drop\s+table/i);
75 | }
76 | };
77 |
78 | Now in your onInputInvalid callback, you can check for "no-troll" in the errors
79 | array, and do something to the troll you caught. ;7
80 |
81 | A validator can optionally have a 'postMatch' method that will be called upon a successful test, if the test returned the result of a call to String.match. It will be passed the match data, and the input that was tested. This is useful for reformatting valid input a format of your choice, for normalization.
82 |
83 | ## Asynchronous Validators
84 |
85 | If a validator object definition includes async: true, that test will be treated as an asynchronous test. This is typically used for validations that require a call to your server. The test function is passed the input (as usual) and a method to pass your test result as a boolean. Note: asynchronous validators only run after all other validators on an input have passed (this is to save on network calls and general complexity). Here is an example:
86 |
87 | ValidateSimple.Validators['password-dictionary'] = {
88 | async: true,
89 | wait: 100,
90 | test: function(input, handeResult){
91 | new Request({
92 | url: '/check_password',
93 | data: { password: input.get('value') },
94 | onSuccess: function(resp){
95 | handeResult(resp !== 'false', true);
96 | }
97 | }).send();
98 | }
99 | };
100 |
101 | You may also specify wait:N in your validator definition to wait Nms after the last successful (synchronous) validation attempt to call your asynchronous validator.
102 |
103 |
104 | ValidateSimple Method: constructor
105 | ----------------------------------
106 |
107 | var vs = new ValidateSimple(form[, options]);
108 |
109 | #### Options
110 |
111 | * active - (boolean: defaults to true) Doesn't attach events until activated.
112 | * validateOnSubmit - (boolean: defaults to true) validate all inputs on submit and fire events (see below).
113 | * inputSelector - (mixed: defaults to 'input') CSS Selector or input elements.
114 | * invalidClass - (string: defaults to 'invalid') class to add for invalid inputs.
115 | * validClass - (string: defaults to 'valid') class to add for valid inputs.
116 | * optionalClass - (string: defaults to 'optional') elements with this class are ignored.
117 | * attributeForType - (string: defaults to 'class') attribute that holds validate-xxx stuff.
118 | * alertEvent - (string: defaults to 'blur') event name for initial alert of input validity.
119 | * correctionEvent - (string: defaults to 'keyup') event name for subsequent alerts of input validity.
120 | * validateEvent - (string: defaults to 'keyup') event name for input validation.
121 | * initialValidation - (boolean: defaults to true) validate all inputs on instantiation.
122 | * alertUnedited - (boolean: defaults to true) validate/alert inputs that have not been edited.
123 | * alertPrefilled - (boolean: defaults to true) validate/alert inputs that have a value on page load.
124 | * validateFieldsets - (boolean: defaults to false) adds valid/invalid classes to entire fieldsets, based on the inputs in them. Also fires invalidFieldset and validFieldset events and passes the fieldset.
125 | * noValidateKeys - (array) array of key codes (event.key) that will disable validation during keypress.
126 | * checkPeriodical - (number: defaults to 1000) how often to check for changed inputs. This comes
127 | in handy when the user uses and automated form filler like 1Password, which does not fire any
128 | events.
129 |
130 |
131 | #### Events
132 |
133 | * inputValid - When an input becomes valid. Arguments: input element and this.
134 | * inputInvalid - When an input becomes invalid. Arguments: input element, errors array and this.
135 | * inputTouched - When an input is first edited. Arguments: input element and this.
136 | * touched - When form has been edited.
137 | * fieldsetValid - When a fieldset is valid and validateFieldsets option is true.
138 | * fieldsetInvalid - When a fieldset is invalid and validateFieldsets option is true.
139 | * valid - When form is valid.
140 | * invalid - When form is invalid.
141 | * inputChecked - Fires after validation, whether pass or fail. Arguments: input element and this.
142 | * invalidSubmit - (Only when validateOnSubmit is true) When form is submitted and invalid.
143 | Arguments: this instance and the submit event.
144 | * validSubmit - (Only when validateOnSubmit is true) When form is submitted and valid.
145 | Arguments: this instance and the submit event.
146 |
147 |
148 | ValidateSimple Method: activate
149 | -------------------------------
150 |
151 | Activates the instance of ValidateSimple (attaches events and sill start firing events).
152 |
153 | #### Syntax
154 |
155 | vs.activate();
156 |
157 |
158 | ValidateSimple Method: deactivate
159 | ---------------------------------
160 |
161 | Deactivates the instance of ValidateSimple (detaches events and sill start firing events).
162 |
163 | #### Syntax
164 |
165 | vs.deactivate();
166 |
167 |
168 | ValidateSimple Method: validateAllInputs
169 | ---------------------------------
170 |
171 | Validates all inputs and returns true for a valid form, false for an invalid form.
172 |
173 | #### Syntax
174 |
175 | vs.validateAllInputs();
176 |
177 |
--------------------------------------------------------------------------------
/Source/ValidateSimple.js:
--------------------------------------------------------------------------------
1 | /*
2 | ---
3 |
4 | name: ValidateSimple
5 | script: ValidateSimple.js
6 | description: Simple form validation with good UX
7 |
8 | requires:
9 | - Core/Class.Extras
10 | - Core/Element.Event
11 | - More/Events.Pseudos
12 | - More/Element.Event.Pseudos
13 | - More/Class.Binds
14 |
15 | provides: [ValidateSimple]
16 |
17 | authors:
18 | - Ian Collins
19 |
20 | ...
21 | */
22 |
23 | var ValidateSimple = new Class({
24 |
25 | Implements: [Events, Options],
26 |
27 | Binds: ['checkValid', 'onSubmit'],
28 |
29 | options: {
30 | active: true,
31 | validateOnSubmit: true,
32 | initialValidation: true,
33 | alertPrefilled: true,
34 | alertUnedited: true,
35 | validateFieldsets: false,
36 | inputSelector: 'input',
37 | invalidClass: 'invalid',
38 | validClass: 'valid',
39 | optionalClass: 'optional',
40 | attributeForType: 'class',
41 | alertEvent: 'blur',
42 | correctionEvent: 'keyup:filterInvalidKeys',
43 | validateEvent: 'keyup:filterInvalidKeys',
44 | checkPeriodical: 500,
45 | noValidateKeys: ['left','right','up','down','esc','tab','command','option','control']
46 | },
47 |
48 | state: 'untouched',
49 |
50 | initialize: function(element, options){
51 | this.setOptions(options);
52 |
53 | this.element = document.id(element).addClass('untouched');
54 | this.parentForm = this.element.get('tag') == 'form' ? this.element : this.element.getParent('form');
55 | this.inputs = this.options.inputs || this.element.getElements(this.options.inputSelector);
56 |
57 | this.element.store('validate-simple-instance', this);
58 |
59 | this.inputs = this.inputs.filter(function(input){
60 | return !input.hasClass(this.options.optionalClass) && !input.get('disabled');
61 | }, this);
62 |
63 | Event.definePseudo('filterInvalidKeys', function(split, fn, args){
64 | if (!this.options.noValidateKeys.contains(args[0].key))
65 | fn.apply(this, args);
66 | }.bind(this));
67 |
68 | if (this.options.active) this.activate();
69 | if (this.options.initialValidation) this.validateAllInputs();
70 |
71 | return this;
72 | },
73 |
74 | attach: function(){
75 | if (!this.active){
76 | this.active = true;
77 |
78 | $(document.body).addEvent('keydown:relay(' + this.options.inputSelector + ')', function(e){
79 | if (e.key !== 'tab' && this.options.noValidateKeys.contains(e.key)){
80 | this.active = false;
81 | (function(){ this.active = true; }).delay(1000, this);
82 | }
83 | }.bind(this));
84 | $(document.body).addEvent('keyup:relay(' + this.options.inputSelector + ')', function(e){
85 | if (e.key !== 'tab' && this.options.noValidateKeys.contains(e.key))
86 | (function(){ this.active = true; }).delay(100, this);
87 | }.bind(this));
88 |
89 | this.inputs.each(function(input){
90 | input.addFocusedProperty();
91 |
92 | var validateEvent = input.get('type').test(/select|radio|checkbox/) ? 'change' : this.options.validateEvent;
93 | input.addEvent(validateEvent, function(e){
94 | if (e.key !== 'tab') this.inputTouched(input);
95 | }.bind(this));
96 |
97 | var callbacks = [this.validateInput.pass(input, this), this.alertInputValidity.pass(input, this)];
98 | if (!(Browser.ie8 && input.get('type').test(/checkbox/))) {
99 | input.addEvent(validateEvent, callbacks[0]);
100 | if (validateEvent !== 'change') input.addEvent('change', callbacks[0]);
101 | }
102 | input.addEvent(this.options.alertEvent, callbacks[1]);
103 |
104 | var prevValue = this.getInputValue(input);
105 | input.store('vs-previous-value', prevValue);
106 | if (this.options.alertPrefilled && prevValue){
107 | this.inputTouched(input);
108 | this.validateInput(input);
109 | this.alertInputValidity(input);
110 | }
111 |
112 | input.store('validate-simple-callbacks', callbacks);
113 | input.store('validate-simple-instance', this);
114 | }, this);
115 |
116 | if (this.options.validateOnSubmit)
117 | this.parentForm.addEvent('submit', this.onSubmit);
118 |
119 | if (this.options.checkPeriodical)
120 | this.checkForChangedInputsPeriodical = this.checkForChangedInputs.periodical(this.options.checkPeriodical, this);
121 | }
122 |
123 | return this;
124 | },
125 | detach: function(){
126 | this.active = false;
127 | this.inputs.each(function(input){
128 | var callbacks = input.retrieve('validate-simple-callbacks');
129 | if (callbacks){
130 | input.removeEvent(this.options.validateEvent, callbacks[0]);
131 | input.removeEvent('change', callbacks[0]);
132 | input.removeEvent(this.options.alertEvent, callbacks[1]);
133 | if (callbacks[2])
134 | input.removeEvent(this.options.correctionEvent, callbacks[2]);
135 | }
136 | input.store('validate-simple-watching', false);
137 | }, this);
138 |
139 | if (this.options.validateOnSubmit)
140 | this.parentForm.removeEvent('submit', this.onSubmit);
141 |
142 | clearInterval(this.checkForChangedInputsPeriodical);
143 | return this;
144 | },
145 |
146 | onSubmit: function(e){
147 | if (!this.validateAllInputs()){
148 | if (e) e.preventDefault();
149 | this.fireEvent('invalidSubmit', [this, e]);
150 | this.alertAllInputs();
151 | } else
152 | this.fireEvent('validSubmit', [this, e]);
153 | },
154 |
155 | activate: function(){ this.attach(); },
156 | deactivate: function(){ this.detach(); },
157 |
158 | inputTouched: function(input){
159 | if (!input.retrieve('validate-simple-touched')){
160 | input.store('validate-simple-touched', true);
161 | this.fireEvent('inputTouched', [input, this]);
162 | }
163 | if (this.element.hasClass('untouched'))
164 | this.changeState('touched');
165 | },
166 |
167 | _getValidatorTypesForInput: function(input){
168 | var validatorTypes = input.get(this.options.attributeForType);
169 | if (this.options.attributeForType == 'class'){
170 | var mtch = validatorTypes.match(/validate\-[\w-]+/g);
171 | validatorTypes = (mtch && mtch.length > 0) ? mtch : ['text'];
172 | }
173 | var v = validatorTypes.map(function(vt){ return vt.replace('validate-',''); });
174 | return v;
175 | },
176 | _validatorWasValid: function(input, validatorType, testResult){
177 | var validator = ValidateSimple.Validators[validatorType];
178 | this.removeErrorFromInput(input, validatorType);
179 | if (validator.postMatch)
180 | validator.postMatch(testResult, input);
181 | },
182 | _validatorWasInvalid: function(input, validatorType, shouldAlert){
183 | this.invalidateInput(input, validatorType);
184 | if (shouldAlert) this.alertInputValidity(input);
185 | },
186 |
187 | validateInput: function(input){
188 | if (!this.active || input == undefined || input.retrieve('validate-simple-locked'))
189 | return this;
190 | else if (input.get('tag') == 'option')
191 | return this.validateInput(input.getParent());
192 |
193 | input.store('validate-simple-is-valid', true);
194 |
195 | this._getValidatorTypesForInput(input).each(function(validatorType){
196 | var validator = ValidateSimple.Validators[validatorType],
197 | handleValidatorResult = function(testResult){
198 | testResult ? this._validatorWasValid(input, validatorType, testResult)
199 | : this._validatorWasInvalid(input, validatorType, validator.async);
200 | }.bind(this);
201 |
202 | if (validator.async){
203 | (function(){
204 | if (input.retrieve('validate-simple-is-valid'))
205 | validator.test(input, handleValidatorResult);
206 | }).afterNoCallsIn(validator.wait || 10);
207 | } else {
208 | var testResult = validator.test(input);
209 | handleValidatorResult(testResult);
210 | }
211 | }, this);
212 |
213 | if (input.retrieve('validate-simple-is-valid')){
214 | input.store('validate-simple-errors', null);
215 | this.alertInputValidity(input);
216 | }
217 |
218 | this.fireEvent('inputChecked', [input, this]);
219 |
220 | this.checkValid();
221 | if (this.options.validateFieldsets) this.checkFieldset(input.getParent('fieldset'));
222 | return this;
223 | },
224 | validateAllInputs: function(){
225 | this.inputs.each(function(input){
226 | this.validateInput(input);
227 | }, this);
228 | return this.state == 'valid';
229 | },
230 |
231 | addErrorToInput: function(input, error){
232 | var errors = input.retrieve('validate-simple-errors') || [];
233 | input.store('validate-simple-errors', errors.include(error));
234 | },
235 | removeErrorFromInput: function(input, error){
236 | var errors = input.retrieve('validate-simple-errors');
237 | if (errors && errors.length > 0)
238 | input.store('validate-simple-errors', errors.erase(error));
239 | },
240 |
241 | invalidateInput: function(input, validatorType){
242 | if (input.retrieve('validate-simple-locked')) return this;
243 | input.store('validate-simple-is-valid', false);
244 | this.addErrorToInput(input, validatorType);
245 | this.changeState('invalid');
246 | return this;
247 | },
248 | lockInput: function(input){
249 | input.store('validate-simple-locked', true);
250 | return this;
251 | },
252 | unlockInput: function(input){
253 | input.store('validate-simple-locked', false);
254 | return this;
255 | },
256 |
257 | alertInputValidity: function(input){
258 | if (!this.active || input == undefined) return this;
259 |
260 | var inputValid = input.retrieve('validate-simple-is-valid'),
261 | isEdited = this.options.alertUnedited ? true : input.retrieve('validate-simple-touched');
262 |
263 | if (this.state != 'untouched' && isEdited){
264 | if (inputValid){
265 | input.addClass(this.options.validClass).removeClass(this.options.invalidClass);
266 | this.fireEvent('inputValid', [input, this]);
267 | } else {
268 | input.addClass(this.options.invalidClass).removeClass(this.options.validClass);
269 | this.fireEvent('inputInvalid', [input, input.retrieve('validate-simple-errors'), this]);
270 | }
271 |
272 | if (!input.retrieve('validate-simple-watching')){
273 | var callback = this.alertInputValidity.pass(input, this);
274 | input.addEvent(this.options.correctionEvent, callback);
275 | input.store('validate-simple-watching', true);
276 | var callbacks = input.retrieve('validate-simple-callbacks') || [];
277 | input.store('validate-simple-callbacks', callbacks.include(callback));
278 | }
279 | }
280 | return this;
281 | },
282 | alertAllInputs: function(){
283 | this.options.alertUnedited = true;
284 | this.inputs.each(function(input){
285 | this.alertInputValidity(input);
286 | }, this);
287 | return this;
288 | },
289 |
290 | getInputValue: function(input){
291 | return input.get('type').test(/radio|checkbox/) ? input.get('checked') : input.get('value');
292 | },
293 |
294 | checkForChangedInputs: function(){
295 | this.inputs.each(function(input){
296 | if (input.retrieve('focused')) return;
297 | var previous = input.retrieve('vs-previous-value'),
298 | current = this.getInputValue(input);
299 |
300 | if (previous != current){
301 | this.inputTouched(input);
302 | this.validateInput(input);
303 | if (!input.retrieve('focused')) this.alertInputValidity(input);
304 | }
305 | input.store('vs-previous-value', current);
306 | }, this);
307 | return this;
308 | },
309 |
310 | checkValid: function(){
311 | var allInputsValidOrOptional = this.inputs.every(function(input){
312 | return input.retrieve('validate-simple-is-valid') || input.hasClass(this.options.optionalClass);
313 | }, this);
314 |
315 | this.changeState(allInputsValidOrOptional ? 'valid' : 'invalid');
316 | return this;
317 | },
318 | checkFieldset: function(fieldset){
319 | if (fieldset){
320 | var valid = fieldset.getElements(this.options.inputSelector).every(function(input){
321 | return input.retrieve('validate-simple-is-valid') || input.hasClass(this.options.optionalClass);
322 | }, this);
323 | if (valid){
324 | fieldset.addClass('valid').removeClass('invalid');
325 | this.fireEvent('fieldSetValid', [fieldset, this]);
326 | } else {
327 | fieldset.addClass('invalid').removeClass('valid');
328 | this.fireEvent('fieldSetInvalid', [fieldset, this]);
329 | }
330 | }
331 | },
332 |
333 | changeState: function(state){
334 | this.state = state;
335 | this.element.addClass(state);
336 | if (state == 'valid') this.element.removeClass('invalid');
337 | else if (state == 'invalid') this.element.removeClass('valid');
338 | else if (state == 'touched') this.element.removeClass('untouched');
339 | this.fireEvent(state, this);
340 | return this;
341 | }
342 |
343 | });
344 |
345 |
346 | ValidateSimple.Validators = {
347 | 'email': {
348 | test: function(input){
349 | return input.get('value').test(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i);
350 | }
351 | },
352 | 'text': {
353 | test: function(input){
354 | var value = input.get('value');
355 | return ((value != null) && (value.length > 0) && value.test(/\S/));
356 | }
357 | },
358 | 'checked': {
359 | test: function(input){
360 | return input.checked;
361 | }
362 | },
363 | 'name': {
364 | test: function(input){
365 | return input.get('value').test(/^[A-Za-z -'&]+$/);
366 | }
367 | },
368 | 'url': {
369 | test: function(input){
370 | return input.get('value').test(/^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i);
371 | }
372 | },
373 | 'alpha': {
374 | test: function(input){
375 | return input.get('value').test(/^[a-zA-Z]+$/);
376 | }
377 | },
378 | 'alphanumeric': {
379 | test: function(input){
380 | var value = input.get('value');
381 | return value.length > 0 && !value.test(/\W/);
382 | }
383 | },
384 | 'numeric': {
385 | test: function(input){
386 | return input.get('value').test(/^-?(?:0$0(?=\d*\.)|[1-9]|0)\d*(\.\d+)?$/);
387 | }
388 | },
389 | 'zipcode': {
390 | test: function(input){
391 | return input.get('value').test(/^\d{5}(-?\d{4})?$/);
392 | }
393 | },
394 | 'state': {
395 | test: function(input){
396 | var states = ['AL','AK','AS','AZ','AR','AE','AA','AE','AP','CA','CO','CT','DE','DC','FM','FL','GA','GU','HI','ID','IL','IN','IA','KS','KY','LA','ME','MH','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY','NC','ND','MP','OH','OK','OR','PW','PA','PR','RI','SC','SD','TN','TX','UT','VT','VI','VA','WA','WV','WI','WY'],
397 | value = input.get('value').clean().toUpperCase();
398 | if (states.contains(value))
399 | return value;
400 | }
401 | }
402 | };
403 |
404 | Event.Keys['command'] = 91;
405 | Event.Keys['option'] = 18;
406 | Event.Keys['shift'] = 16;
407 | Event.Keys['control'] = 17;
408 |
409 | Element.implement({
410 | addFocusedProperty: function(d){
411 | var delay = 500;
412 | if (d !== undefined) delay = d;
413 | this.store('focused', false);
414 | this.addEvent('focus', (function(){ this.store('focused', true); }).bind(this));
415 | this.addEvent('blur', (function(){ this.store.delay(delay, this, ['focused', false]); }));
416 | }
417 | });
418 |
419 | Function.implement({
420 | afterNoCallsIn: function(time, bind, args){
421 | clearTimeout(this._afterNoCallsInDelayId);
422 | this._afterNoCallsInDelayId = this.delay(time, bind, args);
423 | }
424 | });
425 |
--------------------------------------------------------------------------------
/package.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: "ValidateSimple"
3 | description: "Simple form validation with good UX"
4 | category: "Forms"
5 | tags:
6 | - "validation"
7 | current: "0.2"
8 | author: "3n"
9 | contact: "ian.collins@gmail.com"
10 | website: "http://www.iancollins.me"
11 | demo: "https://www.banksimple.com"
12 | sources:
13 | - "Source/ValidateSimple.js"
--------------------------------------------------------------------------------