├── 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 | ![Screenshot](http://idisk.me.com/iancollins/Public/Pictures/Skitch/BankSimple_%7C_Home-20101201-135604.png) 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" --------------------------------------------------------------------------------