A css-class based form validation system.
if (!window.Form) window.Form = {};
var InputValidator = new Class({
Implements: [Options],
options: {
errorMsg: 'Validation failed.',
test: function(field){return true;}
},
initialize: function(className, options){
this.setOptions(options);
this.className = className;
},
test: function(field, props){
if (document.id(field)) return this.options.test(document.id(field), props || this.getProps(field));
else return false;
},
getError: function(field, props){
var err = this.options.errorMsg;
if (typeOf(err) == 'function') err = err(document.id(field), props || this.getProps(field));
return err;
},
getProps: function(field){
if (!document.id(field)) return {};
return field.get('validatorProps');
}
});
Element.Properties.validatorProps = {
set: function(props){
return this.eliminate('$moo:validatorProps').store('$moo:validatorProps', props);
},
get: function(props){
if (props) this.set(props);
if (this.retrieve('$moo:validatorProps')) return this.retrieve('$moo:validatorProps');
if (this.getProperty('$moo:validatorProps')){
try {
this.store('$moo:validatorProps', JSON.decode(this.getProperty('$moo:validatorProps')));
}catch(e){
return {};
}
} else {
var vals = this.get('class').split(' ').filter(function(cls){
return cls.test(':');
});
if (!vals.length){
this.store('$moo:validatorProps', {});
} else {
props = {};
vals.each(function(cls){
var split = cls.split(':');
if (split[1]){
try {
props[split[0]] = JSON.decode(split[1]);
} catch(e){}
}
});
this.store('$moo:validatorProps', props);
}
}
return this.retrieve('$moo:validatorProps');
}
};
Form.Validator = new Class({
Implements:[Options, Events],
Binds: ['onSubmit'],
options: {/*
onFormValidate: function(isValid, form, event){},
onElementValidate: function(isValid, field, className, warn){},
onElementPass: function(field){},
onElementFail: function(field, validatorsFailed){}, */
fieldSelectors: 'input, select, textarea',
ignoreHidden: true,
ignoreDisabled: true,
useTitles: false,
evaluateOnSubmit: true,
evaluateFieldsOnBlur: true,
evaluateFieldsOnChange: true,
serial: true,
stopOnFailure: true,
warningPrefix: function(){
return Form.Validator.getMsg('warningPrefix') || 'Warning: ';
},
errorPrefix: function(){
return Form.Validator.getMsg('errorPrefix') || 'Error: ';
}
},
initialize: function(form, options){
this.setOptions(options);
this.element = document.id(form);
this.element.store('validator', this);
this.warningPrefix = Function.from(this.options.warningPrefix)();
this.errorPrefix = Function.from(this.options.errorPrefix)();
if (this.options.evaluateOnSubmit) this.element.addEvent('submit', this.onSubmit);
if (this.options.evaluateFieldsOnBlur || this.options.evaluateFieldsOnChange) this.watchFields(this.getFields());
},
toElement: function(){
return this.element;
},
getFields: function(){
return (this.fields = this.element.getElements(this.options.fieldSelectors));
},
watchFields: function(fields){
fields.each(function(el){
if (this.options.evaluateFieldsOnBlur)
el.addEvent('blur', this.validationMonitor.pass([el, false], this));
if (this.options.evaluateFieldsOnChange)
el.addEvent('change', this.validationMonitor.pass([el, true], this));
}, this);
},
validationMonitor: function(){
clearTimeout(this.timer);
this.timer = this.validateField.delay(50, this, arguments);
},
onSubmit: function(event){
if (!this.validate(event) && event) event.preventDefault();
else this.reset();
},
reset: function(){
this.getFields().each(this.resetField, this);
return this;
},
validate: function(event){
var result = this.getFields().map(function(field){
return this.validateField(field, true);
}, this).every(function(v){ return v;});
this.fireEvent('formValidate', [result, this.element, event]);
if (this.options.stopOnFailure && !result && event) event.preventDefault();
return result;
},
validateField: function(field, force){
if (this.paused) return true;
field = document.id(field);
var passed = !field.hasClass('validation-failed');
var failed, warned;
if (this.options.serial && !force){
failed = this.element.getElement('.validation-failed');
warned = this.element.getElement('.warning');
}
if (field && (!failed || force || field.hasClass('validation-failed') || (failed && !this.options.serial))){
var validators = field.className.split(' ').some(function(cn){
return this.getValidator(cn);
}, this);
var validatorsFailed = [];
field.className.split(' ').each(function(className){
if (className && !this.test(className, field)) validatorsFailed.include(className);
}, this);
passed = validatorsFailed.length === 0;
if (validators && !field.hasClass('warnOnly')){
if (passed){
field.addClass('validation-passed').removeClass('validation-failed');
this.fireEvent('elementPass', field);
} else {
field.addClass('validation-failed').removeClass('validation-passed');
this.fireEvent('elementFail', [field, validatorsFailed]);
}
}
if (!warned){
var warnings = field.className.split(' ').some(function(cn){
if (cn.test('^warn-') || field.hasClass('warnOnly'))
return this.getValidator(cn.replace(/^warn-/,''));
else return null;
}, this);
field.removeClass('warning');
var warnResult = field.className.split(' ').map(function(cn){
if (cn.test('^warn-') || field.hasClass('warnOnly'))
return this.test(cn.replace(/^warn-/,''), field, true);
else return null;
}, this);
}
}
return passed;
},
test: function(className, field, warn){
field = document.id(field);
if ((this.options.ignoreHidden && !field.isVisible()) || (this.options.ignoreDisabled && field.get('disabled'))) return true;
var validator = this.getValidator(className);
warn = warn != null ? warn : false;
if (field.hasClass('warnOnly')) warn = true;
var isValid = field.hasClass('ignoreValidation') || (validator ? validator.test(field) : true);
if (validator && field.isVisible()) this.fireEvent('elementValidate', [isValid, field, className, warn]);
if (warn) return true;
return isValid;
},
resetField: function(field){
field = document.id(field);
if (field){
field.className.split(' ').each(function(className){
if (className.test('^warn-')) className = className.replace(/^warn-/, '');
field.removeClass('validation-failed');
field.removeClass('warning');
field.removeClass('validation-passed');
}, this);
}
return this;
},
stop: function(){
this.paused = true;
return this;
},
start: function(){
this.paused = false;
return this;
},
ignoreField: function(field, warn){
field = document.id(field);
if (field){
this.enforceField(field);
if (warn) field.addClass('warnOnly');
else field.addClass('ignoreValidation');
}
return this;
},
enforceField: function(field){
field = document.id(field);
if (field) field.removeClass('warnOnly').removeClass('ignoreValidation');
return this;
}
});
Form.Validator.getMsg = function(key){
return Locale.get('FormValidator.' + key);
};
Form.Validator.adders = {
validators:{},
add : function(className, options){
this.validators[className] = new InputValidator(className, options);
if this is a class (this method is used by instances of Form.Validator and the Form.Validator namespace) extend these validators into it this allows validators to be global and/or per instance
if (!this.initialize){
this.implement({
validators: this.validators
});
}
},
addAllThese : function(validators){
Array.from(validators).each(function(validator){
this.add(validator[0], validator[1]);
}, this);
},
getValidator: function(className){
return this.validators[className.split(':')[0]];
}
};
Object.append(Form.Validator, Form.Validator.adders);
Form.Validator.implement(Form.Validator.adders);
Form.Validator.add('IsEmpty', {
errorMsg: false,
test: function(element){
if (element.type == 'select-one' || element.type == 'select')
return !(element.selectedIndex >= 0 && element.options[element.selectedIndex].value != '');
else
return ((element.get('value') == null) || (element.get('value').length == 0));
}
});
Form.Validator.addAllThese([
['required', {
errorMsg: function(){
return Form.Validator.getMsg('required');
},
test: function(element){
return !Form.Validator.getValidator('IsEmpty').test(element);
}
}],
['minLength', {
errorMsg: function(element, props){
if (typeOf(props.minLength))
return Form.Validator.getMsg('minLength').substitute({minLength:props.minLength,length:element.get('value').length });
else return '';
},
test: function(element, props){
if (typeOf(props.minLength)) return (element.get('value').length >= props.minLength || 0);
else return true;
}
}],
['maxLength', {
errorMsg: function(element, props){
props is {maxLength:10}
if (typeOf(props.maxLength))
return Form.Validator.getMsg('maxLength').substitute({maxLength:props.maxLength,length:element.get('value').length });
else return '';
},
test: function(element, props){
if the value is <= than the maxLength value, element passes test
return (element.get('value').length <= props.maxLength || 10000);
}
}],
['validate-integer', {
errorMsg: Form.Validator.getMsg.pass('integer'),
test: function(element){
return Form.Validator.getValidator('IsEmpty').test(element) || (/^(-?[1-9]\d*|0)$/).test(element.get('value'));
}
}],
['validate-numeric', {
errorMsg: Form.Validator.getMsg.pass('numeric'),
test: function(element){
return Form.Validator.getValidator('IsEmpty').test(element) ||
(/^-?(?:0$0(?=\d*\.)|[1-9]|0)\d*(\.\d+)?$/).test(element.get('value'));
}
}],
['validate-digits', {
errorMsg: Form.Validator.getMsg.pass('digits'),
test: function(element){
return Form.Validator.getValidator('IsEmpty').test(element) || (/^[\d() .:\-\+#]+$/.test(element.get('value')));
}
}],
['validate-alpha', {
errorMsg: Form.Validator.getMsg.pass('alpha'),
test: function(element){
return Form.Validator.getValidator('IsEmpty').test(element) || (/^[a-zA-Z]+$/).test(element.get('value'));
}
}],
['validate-alphanum', {
errorMsg: Form.Validator.getMsg.pass('alphanum'),
test: function(element){
return Form.Validator.getValidator('IsEmpty').test(element) || !(/\W/).test(element.get('value'));
}
}],
['validate-date', {
errorMsg: function(element, props){
if (Date.parse){
var format = props.dateFormat || '%x';
return Form.Validator.getMsg('dateSuchAs').substitute({date: new Date().format(format)});
} else {
return Form.Validator.getMsg('dateInFormatMDY');
}
},
test: function(element, props){
if (Form.Validator.getValidator('IsEmpty').test(element)) return true;
var d;
if (Date.parse){
var format = props.dateFormat || '%x';
d = Date.parse(element.get('value'));
var formatted = d.format(format);
if (formatted != 'invalid date') element.set('value', formatted);
return !isNaN(d);
} else {
var regex = /^(\d{2})\/(\d{2})\/(\d{4})$/;
if (!regex.test(element.get('value'))) return false;
d = new Date(element.get('value').replace(regex, '$1/$2/$3'));
return (parseInt(RegExp.$1, 10) == (1 + d.getMonth())) &&
(parseInt(RegExp.$2, 10) == d.getDate()) &&
(parseInt(RegExp.$3, 10) == d.getFullYear());
}
}
}],
['validate-email', {
errorMsg: Form.Validator.getMsg.pass('email'),
test: function(element){
return Form.Validator.getValidator('IsEmpty').test(element) || (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i).test(element.get('value'));
}
}],
['validate-url', {
errorMsg: Form.Validator.getMsg.pass('url'),
test: function(element){
return Form.Validator.getValidator('IsEmpty').test(element) || (/^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i).test(element.get('value'));
}
}],
['validate-currency-dollar', {
errorMsg: Form.Validator.getMsg.pass('currencyDollar'),
test: function(element){
[$]1[##][,###]+[.##] [$]1###+[.##] [$]0.## [$].##
return Form.Validator.getValidator('IsEmpty').test(element) || (/^\$?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/).test(element.get('value'));
}
}],
['validate-one-required', {
errorMsg: Form.Validator.getMsg.pass('oneRequired'),
test: function(element, props){
var p = document.id(props['validate-one-required']) || element.getParent(props['validate-one-required']);
return p.getElements('input').some(function(el){
if (['checkbox', 'radio'].contains(el.get('type'))) return el.get('checked');
return el.get('value');
});
}
}]
]);
Element.Properties.validator = {
set: function(options){
var validator = this.retrieve('validator');
if (validator) validator.setOptions(options);
return this.store('$moo:validator:options');
},
get: function(options){
if (options || !this.retrieve('validator')){
if (options || !this.retrieve('$moo:validator:options')) this.set('validator', options);
this.store('validator', new Form.Validator(this, this.retrieve('$moo:validator:options')));
}
return this.retrieve('validator');
}
};
Element.implement({
validate: function(options){
this.set('validator', options);
return this.get('validator', options).validate();
}
});
<1.2compat> legacy
var FormValidator = Form.Validator;
</1.2compat>