Skip to content

Instantly share code, notes, and snippets.

@nmcgann
Last active February 10, 2017 16:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nmcgann/58ed5d3e3205b1cd19afaf6e8b08e7ad to your computer and use it in GitHub Desktop.
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).
/**
* 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;
}
};
@nmcgann
Copy link
Author

nmcgann commented Dec 6, 2016

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.

@nmcgann
Copy link
Author

nmcgann commented Jan 13, 2017

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.

@nmcgann
Copy link
Author

nmcgann commented Feb 10, 2017

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