February 13, 2008

Exciting Frontend Form Validation

This is part one in my new series, The Living Web: bringing the boring to life

Here is my idea. When a user enters information into a contact form, or some other form, (like a survey) if they screw up, I want red arrows to fly down and show them what they messed up on. This will happen every time they hit the submit button until they figure out what the crap they are doing wrong.

The design pattern:
  • The form submit button gets hi-jacked by custom validation methods
  • Each input is assigned a new red arrow which is hidden from view
  • User hits submit button
  • The form submit event is stopped with YAHOO.util.Event.stopEvent(e);
  • Each input's value is checked against a unique validation method
  • If they all check out, the form is submitted
  • If even one input's value is invalid, the red arrow flies down
  • The input flashes red
  • A new event is made waiting for focus on the input box
  • When the new event is fired, the arrow disappears, we assume user will fix problem
  • The arrow is reset
  • The new event waiting for focus on that input box must be deleted
  • The user "fixes" the problem
  • The user tries submitting the form again
This must work unlimited times in a row.

Here is the code I came up with:

(function(el) {

/* GLOBAL Variables
* invalid : will be an array of id's of inputs which did not validate
* form: will contain name of form i.e. document.formName */
var invalid, form;

/* these are the properties of our red arrow image */
var arrowImage = {
'url' : 'images/arrow.png',
'width' : 66,
'height' : 28
}

/* inputs is the set of unique validation methods */
var inputs = {
'input_1': {
'message': 'string',
'validates': function(value) {
return (value.length > 0);
}
},

'input_2': {
'message': 'string',
'validates': function(value) {
return (value.length > 0);
}
},

'input_3': {
'message': 'must be an email address',
'validates': function(value) {
/* taken from http://www.quirksmode.org/js/mailcheck.html, thankyou! */
var filter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
return filter.test(value);
}
}
};

var validateForm = function(e) {

/* we can't have the form submitting yet */
Event.stopEvent(e);

/* reset this array, because each time the
* user clicks submit, we assume there is
* nothing wrong */
invalid = [];

for(var i in inputs) {
/* save a reference to actual html element */
inputs[i]['el'] = $(i);

/* clearly, this is the value of the curent input */
var value = inputs[i]['el']['value'];

/* this is where we test each input's value with that
* input's specific validation method */
if(!inputs[i]['validates'](value)) {
invalid.push(i);
}
}
/* if invalid is empty
* i.e. it's length is 0,
* there was nothing wrong,
* meaning form can be submitted */
if(invalid.length > 0) {

/* announce is another word meaning
* red arrows will fly down */
announceErrors();
}
else {
/* we got the form name in the
* Event.onAvailable callback earlier (below),
* so submit the form */
document.eval(form).submit();
}
};

var announceErrors = function() {
for(var i = 0; i < invalid.length; i++) {

/* get the id of the input which has the invalid value */
var input = inputs[invalid[i]];
var region = Dom.getRegion(input['el']);

/* begin to show the arrow to the user */
Dom.setStyle(input['arrow'], 'visibility', 'visible');

/* use math to figure out where the arrow should go */
var slideArrow = new Anim(input['arrow'], {
'top' : { 'to' : region['top'] - (arrowImage['height'] * 3/4)},
'left' : { 'to' : region['left'] - arrowImage['width']}
}, 0.25, YAHOO.util.Easing.bounceOut);

/* when the arrow stops sliding, the input box should flash red */
slideArrow.onComplete.subscribe(flashInputs, i);
slideArrow.animate();

/* here is where we make the new
* event to wait for user's focus
* please study what is happening here
* we are saying, when the html element gets focus,
* call fadeArrow, because the arrow should go away not,
* because we assume the user will fix the issue
* Then we say, when you call fadeArrow, give it some stuff
* give it an object with two things,
* 1) the html element itself
* (so we can remove it's focus listener)
* 2) give it the arrow element
* then the true signifies that that object
* with the two things should be the scope of
* fadeArrow, in other words, when fadeArrow gets
* called, this['arrow'] will be same as saying,
* input['arrow'] here, whew */
Event.on(input['el'], 'focus', fadeArrow, {
'obj': input['el'],
'arrow': input['arrow']
}, true);
}
};

var flashInputs = function(e, o, i) {

/* we allow e,o, and i,
* only because we need to use i,
* can you figure out why we did not want
* i to be the scope aka, 'this' ? */
var id = invalid[i];

var red = new ColorAnim(inputs[id]['el'], {
'backgroundColor': { 'to' : '#B91309' },
'color' : {'to' : '#FFF'}
}, 0.2);

red.onComplete.subscribe(function() {
var white = new ColorAnim(inputs[id]['el'], {
'backgroundColor': { 'to' : '#FFF' },
'color' : {'to' : '#000'}
}, 0.2);
white.animate();
});
red.animate();
};

var fadeArrow = function() {

/* input box has recieved focus,
* recall that 'this' refers to
* the object with 'obj' and 'arrow'
* remove that listener */
Event.removeListener(this['obj'], 'focus', fadeArrow);

var fadeOut = new Anim(this['arrow'], {
'opacity' : { 'to' : 0 }
}, 0.5);

fadeOut.onComplete.subscribe(resetArrow, this['arrow'], true);
fadeOut.animate();
};

var resetArrow = function() {
setStyles(this, {
'top' : '0px',
'left': '0px',
'visibility': 'hidden',
'opacity' : 1
});
};

var setStyles = function(el, styles) {
for(var s in styles) {
Dom.setStyle(el, s, styles[s]);
}
};

/* basically the constructor */
Event.onAvailable(el, function() {

for(var i in inputs) {
/* give each input its own arrow element */
var arrow = document.createElement('div');
$('doc').appendChild(arrow);
Dom.addClass(arrow, 'arrow');
setStyles(arrow, {
'width': arrowImage['width'] + 'px',
'height': arrowImage['height'] + 'px',
'backgroundImage' : 'url(\''+ arrowImage['url'] +'\')'
});
inputs[i]['arrow'] = arrow;
}

/* note that el is html element id (a string)
* of the submit button, but we temporarily
* reference it with form, this save a tiny bit
* of processing time */
form = $(el);
Event.on(form, 'click', validateForm);

/* bubble up the dom tree to find the html form element
* starting at the submit button */
while(form.tagName.toUpperCase() != 'FORM') {
form = form.parentNode;
}
/* get name of form */
form = form['name'];
});
/* this anonymous function self-invokes itself, and gives itself a string
* this string becomes el, which is the html element id of the submit button */
}('submit_button'))


A working example. My questions for you:
  1. Why is the whole thing an anonymous function?
  2. If it were not anonymous, what public properties or methods could it offer for interaction with other javascript classes?
  3. What css rules would need to be applied to the arrow, for the class div.arrow?
  4. Do you like the way I got the name of the form in order to call the submit() method, is there a better way?
  5. How could the code be modified to enable multiple forms?
  6. How could the code be modified to allow abstracted validation methods?
  7. What would you have done differently?
Feel free to leave comments answering any or all of my questions for you. Also, if you want, I can explain more of a certain part for you, no matter who you are.

Thank you!

No comments: