It's pretty much all pluggable; you apply validators following the correct interface and you provide reporters (the view part I guess) following the reporter interface.
Code: Select all
/**
* FormValidator library for JavaScript
* Uses a strategy based approach to form validation
* @author Chris Corbyn
* @param {object} HTMLFormElement
* @throws Error
* @constructor
*/
function FormValidator(formElement)
{
if (!(formElement instanceof HTMLFormElement))
{
throw new Error('FormValidator can only operate on HTMLFormElements');
}
/**
* The DOM node of the Form to validate against
* @type Object HTMLFormElement
*/
var __form = formElement;
/**
* The reporter object to use when displaying passes and fails
* @type Object FV_Reporter
*/
var __reporter;
/**
* For Unit Testing only -- disables strict interface type checking
* @type Boolean
*/
var __internalTypeCheck = true;
/**
* Error messages unique to relevant form fields
* @type Array
*/
var __messages = new Array();
/**
* Validator objects applied to each form field
* @type Array
*/
var __validators = new Array();
/**
* Stupid hack needed to get internal callbacks working
* @type Object FormValidator
*/
var myInstance = this;
/**
* This is only used for Unit Testing (unless you know what you're doing!)
*/
this.__disableTypeChecks = function()
{
__internalTypeCheck = false;
}
/**
* Get the actual DOM node of the element with name attribute 'field'
* @param {string} fieldName
* @returns {mixed} Element, Array
*/
this.getFieldElement = function(field)
{
return __form[field];
}
/**
* Return a string representation of a field's type
* @param {string} fieldName
* @returns {string} type
*/
this.getFieldType = function(field)
{
if (f = this.getFieldElement(field))
{
switch (f.constructor)
{
case HTMLInputElement:
return f.type.toLowerCase();
case HTMLSelectElement:
return 'select';
case HTMLTextAreaElement:
return 'textarea';
default:
if ((typeof f.length == 'undefined') && (typeof f.type != 'undefined')) return f.type.toLowerCase();
else if (typeof f.length != 'undefined') return f.item(0).type.toLowerCase();
}
}
}
/**
* Get the value at a given field name
* @param {string} fieldName
* @returns {mixed} String, Array
*/
this.getFieldValue = function (field)
{
switch (this.getFieldType(field))
{
case 'text':
case 'password':
case 'hidden':
case 'submit':
case 'textarea':
return this.getFieldElement(field).value;
case 'checkbox':
if (this.getFieldElement(field).checked)
{
return this.getFieldElement(field).value;
}
break;
case 'select':
if (!this.getFieldElement(field).multiple)
{
return this.getFieldElement(field).options[this.getFieldElement(field).selectedIndex].value;
}
else
{
var ret = new Array();
for (var i = 0, len = this.getFieldElement(field).options.length; i < len; i++)
{
if (this.getFieldElement(field).options[i].selected)
{
ret.push(this.getFieldElement(field).options[i].value);
}
}
return ret;
}
break;
case 'radio':
for (var i = 0, len = this.getFieldElement(field).length; i < len; i++)
{
if (this.getFieldElement(field).item(i).checked) return this.getFieldElement(field).item(i).value;
}
break;
case 'checkbox':
if (this.getFieldElement(field).checked)
{
return this.getFieldElement(field).value;
}
else return false;
}
}
/**
* Apply a new Strategy object to the FormValidator
* @param {object} FV_Validator
* @param {string} fieldName
* @param {string} errorMessage
*/
this.apply = function(validatorObj, field, message)
{
if (__internalTypeCheck && !(validatorObj instanceof FV_Validator))
{
throw new Error('FormValidator.apply requires parameter 1 to be object of type FV_Validator');
}
if (typeof field != 'string')
{
throw new Error('FormValidator.apply requires parameter 2 to be string');
}
__setMessage(field, message);
__setValidator(field, validatorObj, this);
}
/**
* Set the reporter to use when displaying errors (or passes)
* @param {object} FV_Reporter
*/
this.setReporter = function(reporter)
{
if (__internalTypeCheck && !(reporter instanceof FV_Reporter))
{
throw new Error('FormValidator.setReporter requires objects of type FV_Reporter');
}
__reporter = reporter;
}
/**
* Set an error message for a given form field
* @param {string} fieldName
* @param {string} errorMessage
*/
var __setMessage = function(field, message)
{
__messages[field] = message;
}
/**
* Set a validator object to use for a given form field
* @param {string} fieldName
* @param {object} FV_Validator
* @param {object} FormValidator
*/
var __setValidator = function(field, obj, parentObj)
{
obj.setValidator(parentObj);
__validators[field] = obj;
}
/**
* Execute all checks
*/
this.run = function()
{
var passed = true;
if (!__reporter)
{
throw new Error('FormValidator.run cannot be invoked before a reporter has been set');
}
for (var field in __validators)
{
if (!myInstance.getFieldElement(field)) continue;
if (__validators[field].check(field)) __reporter.pass(field);
else
{
__reporter.fail(field, __messages[field]);
passed = false;
}
}
return passed;
}
/**
* Attach to the form's onsubmit event and optionally set the reporter
* @param {object} FV_Reporter
*/
this.activate = function(reporter)
{
if (document.captureEvents) document.captureEvents(Event.SUBMIT);
__form.onsubmit = this.run;
if (reporter) this.setReporter(reporter);
}
}
/**
* Abstract class FV_Validator
* Provides some common functionality for strategy-based validators
* and enforces an interface
* @constructor
*/
function FV_Validator()
{
/**
* The Parent validator object
* @type Object FormValidator
*/
this.__validator;
/**
* Set the FormValidator instance
* @param {object} FormValidator
*/
this.setValidator = function(validatorObj)
{
this.__validator = validatorObj;
}
/**
* Abstract method check()
* Return a boolean response corresponding to the validity of the field
* @param {string} fieldName
* @returns {boolean}
*/
this.check = function(field)
{
throw new Error('FV_Validator.check is abstract and must be overridden by child');
}
}
/**
* Abstract class FV_Reporter
* Enforces the interface any reporter classes must implement
* @constructor
*/
function FV_Reporter()
{
/**
* Paint, or deal with a passing validation
*/
this.pass = function(field) {}
/**
* Paint, or deal with a failed validation (abstract)
*/
this.fail = function(field, message)
{
throw new Error('FV_Reporter.fail is abstract and most be overridden by child');
}
}
////////////////////////////////////////////////////////
// FormValidator validation classes. All classes must //
// inherit from FV_Validator and implement //
// abstract method FV_Validator.check() //
////////////////////////////////////////////////////////
/**
* A basic check to ensure a value has been given
* @constructor
*/
function FV_NonEmptyValidator()
{
this.check = function(field)
{
if (v = this.__validator.getFieldValue(field))
{
if (typeof v.length != 'undefined' && v.length > 0) return true;
}
else return false;
}
} FV_NonEmptyValidator.prototype = new FV_Validator;
/**
* A regular expression validation
* @constructor
*/
function FV_RegexValidator(pattern)
{
if (!(pattern instanceof RegExp))
{
throw new Error('FV_RegexValidator expects constructor parameter 1 to be of type RegExp');
}
this.check = function(field)
{
if (v = this.__validator.getFieldValue(field))
{
if (typeof v.length != 'undefined' && pattern.test(v.toString())) return true;
}
else return false;
}
} FV_RegexValidator.prototype = new FV_Validator;
////////////////////////////////////////////////////////
// A handful of reporters to use with FormValidator //
// All reporters must inherit from abstract class //
// FV_Reporter and implement at least fail() //
////////////////////////////////////////////////////////
/**
* Display error messages in a DOM element on the page
* @constructor
* @param {object} Element
*/
function FV_PageElementReporter(element)
{
if (!(element instanceof Element))
{
throw new Error('FV_PageElementReporter expects constructor parameter 1 to be object of type Element');
}
/**
* The DOM element to display errors in
* @type Element
*/
this.element = element;
/**
* Errors already outputted to page
* @type Array
*/
this.outputted = new Array();
/**
* A list of active field errors
* @type Array
*/
this.active = new Array();
/**
* Remove errors if they now pass validation
* @param {string} field
*/
this.pass = function(field)
{
if (typeof this.outputted[field] != 'undefined')
{
try {
this.element.removeChild(this.outputted[field]);
delete this.active[field];
} catch (e) {
//
}
}
}
/**
* Overridden abstract method fail()
* @param {string} field
* @param {string} message
*/
this.fail = function(field, message)
{
if (typeof this.outputted[field] == 'undefined')
{
this.outputted[field] = document.createElement('div');
this.outputted[field].style.padding = '4px';
}
if (!this.active[field])
{
this.element.appendChild(this.outputted[field]);
this.active.push(field);
}
this.outputted[field].innerHTML = message;
}
} FV_PageElementReporter.prototype = new FV_Reporter;
Code: Select all
function TestOfFormValidator()
{
this.getTestForm = function()
{
var f = document.createElement('form');
var i1 = document.createElement('input');
i1.type = 'text';
i1.name = 'text_input';
i1.value = 'some_value';
f.appendChild(i1);
var i2 = document.createElement('input');
i2.type = 'password';
i2.name = 'password_input';
i2.value = '';
f.appendChild(i2);
var i3 = document.createElement('select');
i3.name = 'select_input';
i3_o1 = document.createElement('option');
i3_o1.value = '42';
i3_o1.text = 'Option 42';
i3.appendChild(i3_o1);
i3_o2 = document.createElement('option');
i3_o2.value = '10';
i3_o2.text = 'Option 10';
i3.appendChild(i3_o2);
i3.selectedIndex = 1;
f.appendChild(i3);
var i4_o1 = document.createElement('input');
i4_o1.type = 'radio';
i4_o1.name = 'radio_input';
i4_o1.value = 'value 1';
var i4_o2 = document.createElement('input');
i4_o2.type = 'radio';
i4_o2.name = 'radio_input';
i4_o2.value = 'value 2';
i4_o2.checked = true;
f.appendChild(i4_o1); f.appendChild(i4_o2);
var i5 = document.createElement('input');
i5.type = 'checkbox';
i5.name = 'checkbox_input';
i5.value = 'my value';
i5.checked = false;
f.appendChild(i5);
var i6 = document.createElement('input');
i6.type = 'checkbox';
i6.name = 'checkbox_input2';
i6.value = 'my value 2';
i6.checked = true;
f.appendChild(i6);
var i7 = document.createElement('textarea');
i7.name = 'textarea_input';
i7.value = 'my text < more text';
f.appendChild(i7);
var i8 = document.createElement('select');
i8.name = 'multi_select_input';
i8.multiple = true;
var i8_o1 = document.createElement('option');
i8_o1.value = '1';
i8_o1.text = 'option 1';
i8_o1.selected = true;
i8.appendChild(i8_o1);
var i8_o2 = document.createElement('option');
i8_o2.value = '2';
i8_o2.text = 'option 2';
i8.appendChild(i8_o2);
var i8_o3 = document.createElement('option');
i8_o3.value = '3';
i8_o3.text = 'option 1';
i8_o3.selected = true;
i8.appendChild(i8_o3);
f.appendChild(i8);
return f;
}
this.testFormElementsCanBeAccessedByName = function()
{
var f = this.getTestForm();
var validator = new FormValidator(f);
this.assertIsA(validator.getFieldElement('text_input'), HTMLInputElement);
this.assertIsA(validator.getFieldElement('password_input'), HTMLInputElement);
this.assertIsA(validator.getFieldElement('select_input'), HTMLSelectElement);
this.assertIsA(validator.getFieldElement('radio_input').item(0), HTMLInputElement);
this.assertIsA(validator.getFieldElement('radio_input').item(1), HTMLInputElement);
this.assertIsA(validator.getFieldElement('checkbox_input'), HTMLInputElement);
this.assertIsA(validator.getFieldElement('checkbox_input2'), HTMLInputElement);
this.assertIsA(validator.getFieldElement('textarea_input'), HTMLTextAreaElement);
this.assertIsA(validator.getFieldElement('multi_select_input'), HTMLSelectElement);
this.assertUndefined(validator.getFieldElement('__not_present'));
}
this.testFormElementTypesAreFound = function()
{
var f = this.getTestForm();
var validator = new FormValidator(f);
this.assertEqual('text', validator.getFieldType('text_input'));
this.assertEqual('password', validator.getFieldType('password_input'));
this.assertEqual('select', validator.getFieldType('select_input'));
this.assertEqual('radio', validator.getFieldType('radio_input'));
this.assertEqual('checkbox', validator.getFieldType('checkbox_input'));
this.assertEqual('checkbox', validator.getFieldType('checkbox_input2'));
this.assertEqual('textarea', validator.getFieldType('textarea_input'));
this.assertEqual('select', validator.getFieldType('multi_select_input'));
this.assertUndefined(validator.getFieldType('__not_present'));
}
this.testFieldValuesAreReturnedAccordingToType = function()
{
var f = this.getTestForm();
var validator = new FormValidator(f);
this.assertEqual('some_value', validator.getFieldValue('text_input'));
this.assertEqual('', validator.getFieldValue('password_input'));
this.assertEqual('10', validator.getFieldValue('select_input'));
this.assertEqual('value 2', validator.getFieldValue('radio_input'));
this.assertFalse(validator.getFieldValue('checkbox_input')); //Not checked, so should be false
this.assertEqual('my value 2', validator.getFieldValue('checkbox_input2'));
this.assertEqual('my text < more text', validator.getFieldValue('textarea_input'));
this.assertEqual(new Array('1', '3'), validator.getFieldValue('multi_select_input'));
this.assertUndefined(validator.getFieldValue('__non_existent'));
}
this.testValidatorCheckMethodIsInvoked = function()
{
var f = this.getTestForm();
var validator = new FormValidator(f);
validator.__disableTypeChecks(); //For testing only
function TestValidator()
{
this.check = function() {}
}
TestValidator.prototype = new FV_Validator;
var mockFactory = new Mock(new TestValidator(), this);
var mockValidator = mockFactory.generate();
validator.apply(mockValidator, 'text_input', 'My message');
function TestReporter()
{
this.pass = function() {}
this.fail = function() {}
}
TestReporter.prototype = new FV_Reporter;
validator.setReporter(new TestReporter());
validator.run();
mockValidator.assertCalledOnce('check', new Array('text_input'));
validator.apply(mockValidator, 'password_input', 'My message');
validator.run();
mockValidator.assertCallCount('check', 3); //The one from earlier, and now 2 on top
}
this.testReporterReceivesFailNotifications = function()
{
function TestReporter()
{
this.pass = function() {}
this.fail = function() {}
}
TestReporter.prototype = new FV_Reporter;
var f = this.getTestForm();
var validator = new FormValidator(f);
validator.__disableTypeChecks();
var mockFactory = new Mock(new TestReporter(), this);
var mockReporter = mockFactory.generate();
validator.setReporter(mockReporter);
function TestValidator()
{
this.check = function() {}
}
TestValidator.prototype = new FV_Validator;
var mockFactory = new Mock(new TestValidator(), this);
var mockValidator = mockFactory.generate();
validator.apply(mockValidator, 'text_input', 'My first message');
validator.apply(mockValidator, 'password_input', 'My second message');
mockReporter.assertCalledNever('pass');
mockReporter.assertCalledNever('fail');
mockValidator.setReturnValueAt('check', true, 1);
mockValidator.setReturnValueAt('check', false, 2);
validator.run();
mockReporter.assertCallSequence(new Array('pass', 'fail'));
mockReporter.assertCalledOnce('pass', new Array('text_input'));
mockReporter.assertCalledOnce('fail', new Array('password_input', 'My second message'));
}
}
Code: Select all
<html>
<head>
<script type="text/javascript" src="FormValidator.js"></script>
</head>
<body>
<div>
<div id="errors" style="font-weight: bold; color: red; font-size: 0.8em; font-family: arial,sans-serif"></div>
<form id="foo">
Text: <input type="text" name="my_field" /><br />
Check: <input type="checkbox" name="my_check" />
<input type="submit" name="submit" value="Submit" />
</form>
</div>
<script type="text/javascript">
var validator = new FormValidator(document.getElementById('foo'));
validator.apply(new FV_RegexValidator(/^foobar\d$/), 'my_field', 'You fool, you supposed to use the regex');
validator.apply(new FV_NonEmptyValidator(), 'my_check', 'Tick the box!');
validator.activate(new FV_PageElementReporter(document.getElementById('errors')));
</script>
</body>
</html>