Last active
February 10, 2017 16:29
-
-
Save nmcgann/58ed5d3e3205b1cd19afaf6e8b08e7ad to your computer and use it in GitHub Desktop.
Javascript verify class based on using data atttibutes on the form inputs and submit(s). No jquery, just plain js (ES5).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* JS form data verification routine. Uses data attributes on the field: | |
* "data-verify": regex pattern that must be true for no error. | |
* "data-error": error message. | |
* "data-options": options separated by "|". First is regex parameters i/g/m as per the RegExp constructor, | |
* second is inversion of the test if "not", or "req" if the test must pass and a non-empty field is | |
* not allowed (both req and not can be used if separated by a ";" or space). Third is the name of a special test function when | |
* a regex isn't sufficient to do the test. Test fn must be within ValidateForm (or added to the prototype). | |
* If an option is blank, it can just be left empty e.g. "|req" if there are no regex params, but data-verify | |
* field must be present so the field is recognised as needing verifying. | |
* | |
* New validation fns can be added to the prototype - e.g. "testfn": | |
* | |
* ValidateForm.prototype.testFn = function(val){ | |
* return (val === 'buzz'); | |
* }; | |
* To specify on an input: data-verify="" data-error="The error message!" data-options="||testFn" | |
* | |
* Submit buttons have two optional data attributes: | |
* "data-message" which holds a message to be shown while a form submit is in | |
* progress and "data-options" which can have "noverify" and/or "nodisable" and/or a number separated by "|" character. | |
* "noverify" prevents the form submission being prevented by a fail on a field verify and | |
* "nodisable" prevents the submit button being disabled once a form submit is started. | |
* A number (positive integer) is the number of milliseconds the submit button remains disabled (only when nodisable not set) | |
* | |
* init as: new ValidateForm('#form-id'); etc. | |
*/ | |
"use strict"; | |
function ValidateForm(selector, newOpts){ | |
this.submitButtonEle = undefined; | |
this.opts = this._extend({ | |
errorMessageClass: "field-error", | |
errorMessageTag: "div", | |
inputErrorClass: "input-error", | |
blurMessageDelayShow: 300, //ms | |
blurMessageDelayHide: 300 //ms | |
//buttonSubmittingText: "Sending..." //set to null to disable change | |
}, newOpts || {}); | |
this.theForm = (typeof selector === 'string') ? document.querySelector(selector) : selector; | |
if(!(this.theForm instanceof Element)){ | |
throw "ValidateForm constructor requires a form element as argument."; | |
}; | |
if(this.theForm && this.theForm.nodeType && this.theForm.nodeType === 1 && this.theForm.nodeName === 'FORM'){ | |
this.verEle = this.theForm.querySelectorAll('input[data-verify],textarea[data-verify]'); | |
this.submitEle = this.theForm.querySelectorAll('input[type="submit"],button[type="submit"]'); | |
this._addListeners(); | |
}else{ | |
throw "ValidateForm constructor requires a form element as argument."; | |
} | |
}; | |
ValidateForm.prototype = { | |
constructor: ValidateForm, | |
_setError: function (ele, msg){ | |
var par = ele.parentNode, | |
err = par.querySelector(this.opts.errorMessageTag + "." + this.opts.errorMessageClass); | |
//add message (only if doesn't exist already) as <tag class="errorMessageClass">text</tag> | |
if(!err && msg){ | |
var errorElement = document.createElement(this.opts.errorMessageTag); | |
errorElement.className = this.opts.errorMessageClass; | |
var t = document.createTextNode(msg); | |
errorElement.appendChild(t); | |
ele.parentNode.appendChild(errorElement); | |
//highlight field | |
ele.classList.add(this.opts.inputErrorClass); | |
} | |
}, | |
_clearError: function (ele){ | |
var par = ele.parentNode, | |
err = par.querySelector(this.opts.errorMessageTag + "." + this.opts.errorMessageClass); | |
if(err){ | |
par.removeChild(err); | |
} | |
ele.classList.remove(this.opts.inputErrorClass); | |
}, | |
//public fn clear any error text/highlights showing | |
clearAllErrors: function(){ | |
var elements = this.verEle, | |
self = this; | |
if(elements){ | |
[].forEach.call(elements, function(ele){ | |
self._clearError(ele); | |
}); | |
} | |
}, | |
_verifyField: function(field){ | |
var patt = field.getAttribute('data-verify'), | |
errmsg = field.getAttribute('data-error'), | |
options = field.getAttribute('data-options'), | |
regexOptions = undefined, | |
fn = undefined, | |
invert = false, | |
required = false, | |
success = true, | |
fieldVal = field.value, | |
self = this; | |
if(options){ | |
options = options.split('|'); | |
if(options.length > 0 && (/^[igm]+$/.test(options[0]))) regexOptions = options[0]; | |
if(options.length > 1 && /\bnot\b/i.test(options[1])) invert = true; | |
if(options.length > 1 && /\breq\b/i.test(options[1])) required = true; | |
if(options.length > 2 && options[2] !== '') fn = this[options[2]]; | |
} | |
//console.log(options); | |
//this._clearError(); | |
//console.log(required, invert, field); | |
//if not required, empty fields are OK | |
if(required || (fieldVal !== '')){ | |
if(typeof fn === 'function'){ | |
try{ | |
if(!invert && !fn(fieldVal) || invert && fn(fieldVal)){ | |
//obj._setError(this, errmsg); | |
success = false; | |
} | |
}catch(e){ | |
console.log('Error in extended test function.'); | |
} | |
}else if (patt){ | |
try{ | |
var regex = new RegExp(patt, regexOptions); | |
if(!invert && !regex.test(fieldVal) || invert && regex.test(fieldVal)){ | |
//obj._setError(this, errmsg); | |
success = false; | |
} | |
}catch(e){ | |
console.log('Error in regex pattern or options.'); | |
} | |
} | |
} | |
//console.log(success); | |
//unpleasantly devious fix using closures - delays clearing an error message | |
//so that submit works. Otherwise the button can move out from under the mouse pointer | |
//when the error message is cleared and submit doesn't happen! | |
setTimeout(function(success, field, errmsg){ | |
return function(){ | |
if(success){ | |
self._clearError(field); | |
}else{ | |
self._setError(field, errmsg); | |
} | |
}; | |
}(success, field, errmsg), success ? this.opts.blurMessageDelayShow : this.opts.blurMessageDelayHide); | |
return success; | |
}, | |
_verifyAllFields: function(){ | |
var pass = true; | |
for(var i = 0, len = this.verEle.length; i < len; i++){ | |
if(!this._verifyField(this.verEle[i])){ | |
pass = false; | |
} | |
} | |
return pass; | |
}, | |
_inputListener: function(evt){ | |
//listener | |
this._verifyField.call(this, evt.target); | |
}, | |
_inputListenerHandler: null, | |
_submitAction: function(evt){ | |
//if prevent submit on verify fail is not disabled in data-options, check all | |
var self = this, | |
opts = this.submitButtonEle.getAttribute('data-options'), //null if missing | |
buttonSubmittingText = self.submitButtonEle.getAttribute('data-message'),//self.opts.buttonSubmittingText; | |
noVerify = false, //if set don't verify form fields | |
noDisable = false, //if set don't disable submit button | |
timeoutDisable = 0;//if >0 remove disable submit button after delay | |
if(opts){ | |
opts = opts.toLowerCase().split('|'); | |
noVerify = (opts.indexOf('noverify') !== -1); | |
noDisable = (opts.indexOf('nodisable') !== -1); | |
//collect any button disable release timeouts | |
timeoutDisable = opts.reduce(function(acc, val){ | |
var param = 0; | |
if(val.match(/^\d+$/)){ | |
param = parseInt(val, 10); | |
} | |
return acc + param; | |
},0); | |
//console.log(noVerify, noDisable, timeoutDisable); | |
} | |
if(!noVerify && !this._verifyAllFields()){ | |
//fail on any field, no submit | |
evt.preventDefault(); | |
evt.stopImmediatePropagation(); //prevent other JS submit handers running | |
}else{ | |
//all good - change button/input name (if text) and inhibit further button presses by disabling | |
//add hidden element to replace submit button lost | |
//when an event handler is attached to it | |
var input = document.createElement('input'); | |
input.type = 'hidden'; | |
input.name = self.submitButtonEle.name; | |
input.value = self.submitButtonEle.value || ''; | |
this.theForm.appendChild(input); | |
if(this.submitButtonEle){ | |
if(buttonSubmittingText){ | |
if(this.submitButtonEle.nodeName === 'BUTTON'){ | |
//button submit (value parameter not seen) | |
this.submitButtonEle.innerHTML = buttonSubmittingText; | |
}else{ | |
//input submit | |
this.submitButtonEle.value = buttonSubmittingText; | |
} | |
} | |
if(!noDisable){ | |
this.submitButtonEle.disabled = true; | |
if(timeoutDisable > 0){ | |
//console.log('started disable'); | |
window.setTimeout(function(){ | |
self.submitButtonEle.disabled = false; | |
//console.log('cleared disable'); | |
}, timeoutDisable); | |
}; | |
} | |
} | |
//console.log(self.submitButtonEle); | |
} | |
}, | |
_submitActionHandler: null, | |
_submitListener: function(evt){ | |
//listener | |
this.submitButtonEle = evt.target; | |
}, | |
_submitListenerHandler: null, | |
_addListeners: function(){ | |
var self = this; | |
this._inputListenerHandler = this._inputListener.bind(this); | |
//add event handlers to all data-verify inputs | |
for(var i = 0, len = this.verEle.length; i < len; i++){ | |
this.verEle[i].addEventListener('blur', this._inputListenerHandler, false); | |
} | |
this._submitListenerHandler = this._submitListener.bind(this); | |
//catch & save the submit button target that submitted the form on click | |
if(this.submitEle){ | |
for(var i = 0, len = this.submitEle.length; i < len; i++){ | |
this.submitEle[i].addEventListener('click', this._submitListenerHandler, false); | |
} | |
} | |
this._submitActionHandler = this._submitAction.bind(this); | |
//check all on submit | |
this.theForm.addEventListener('submit', this._submitActionHandler, false); | |
}, | |
//public destroy call to remove event handlers and clear any errors | |
destroy: function(){ | |
//remove event handlers to all data-verify inputs | |
for(var i = 0, len = this.verEle.length; i < len; i++){ | |
this.verEle[i].removeEventListener('blur', this.inputListenerHandler, false); | |
}; | |
//remove submit button handlers | |
if(this.submitEle){ | |
for(var i = 0, len = this.submitEle.length; i < len; i++){ | |
this.submitEle[i].removeEventListener('click', this.submitListenerHandler, false); | |
}; | |
}; | |
//remove form submit handlers | |
this.theForm.removeEventListener('submit', this.submitActionHandler, false); | |
this.clearAllErrors(); | |
}, | |
//validate email | |
validateEmail: function (email) { | |
//from stackoverflow | |
var re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; | |
return re.test(email); | |
}, | |
//check an isodate is valid format and in terms of days per month etc. | |
validateISODate: function(date){ | |
var isValid = true, year, month, day, | |
matches = date.match(/^(\d{4})-(\d{2})-(\d{2})$/); | |
//check for basic dddd-dd-dd format (d=digit) | |
if(matches === null){ | |
return false; | |
}else{ | |
year = parseInt(matches[1], 10); | |
month = parseInt(matches[2], 10); | |
day = parseInt(matches[3], 10); | |
}; | |
//check day not obviously wrong then check month and resulting days | |
if(day >= 1 && day <= 31){ | |
switch(month){ | |
case 1: case 3: case 5: case 7: case 8: case 10: case 12: | |
break; | |
case 4: case 6: case 9: case 11: | |
if(day > 30){ | |
isValid = false; | |
}; | |
break; | |
case 2: | |
if( (year % 4 === 0) && ( (year % 100 !== 0) || (year % 400 === 0) ) ){ | |
if(day > 29){ | |
isValid = false; | |
}; | |
} else if(day > 28){ | |
isValid = false; | |
}; | |
break; | |
default: //illegal month | |
isValid = false; | |
}; | |
}else{ | |
isValid = false; | |
} | |
return isValid; | |
}, | |
//utility fn | |
_extend: function(obj, src) { | |
Object.keys(src).forEach(function(key) { obj[key] = src[key]; }); | |
return obj; | |
} | |
}; |
Had to add evt.stopImmediatePropagation();
to handle case where there was another JS handler on the submit. Verify was failing, but the form was still being submitted without that mod.
Fixed several bugs, added blur delay parameters. Added destroy() method.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quite useful example of JS classes, event handlers in classes (
this
manipulation using.call()
), not using jQuery etc.Note the annoying fix using timeouts to stop a clicked submit button moving out from under a mouse pointer when error messages get cleared.