« Back to Index

Mega Validation Form

View original Gist on GitHub

validate.js

define(["../Templating/hogan", "../XHR/ajax", "../Array/map", "../DOM/getEl"], function (hogan, ajax, map, getElement) {
	
	function validate (formname) {
		var template,
			doc = document,
			module = getElement("js-alreadymember"),
			insert_errors = getElement("js-formerrors"),
			pin_error = getElement("js-incorrectpin"),
			errors = [],
			form = document.forms[formname],
			fields = form.elements,
			formats = {
				number: function (field) {
					if (/^\D+/.test(field.value) || !field.value.length) {
						errors.push(field);
					} else if (field.name === "pin") {
						// We call script which tells us whether the data added by the user is correct or not
						ajax({
							url: "Assets/PHP/pin.php",
							async: false, // first time ever I've needed to use a 'synchronous' AJAX call
							method: "POST",
							data: "pin=" + field.value + "", // we're posting the user's entered pin to the server
							onSuccess: function (data) {
								// 'data' is returned as a String which causes issues with coercion (as a string of any length, even if it contains zero, is still a truthy value)
								// So we use the + operator to convert the string into a number
								if (+data) {
									pin_error.className = "hide";
								} else {
									errors.push(field);
									pin_error.className = "";
								}
							}
						});
					}
				},
				date: function (field) {
					/*
						The regex allows the following formats:
							00/00/0000
							00/00/00
							0/0/00
					 */
					if (!/^\d{1,2}\/\d{1,2}\/\d{2,4}$/.test(field.value) || !field.value.length) {
    				    errors.push(field);
					}
				},
				email: function (field) {
					if (field.value.indexOf("@") === -1 || !field.value.length) {
						errors.push(field);
					}
				},
				mobile: function (field) {
					/*
						The regex allows the following formats:
							+44 07000000000
							07000000000
							+4407000000000
						
						So there is an optional +000 country code at the start (wrapped in a non-capturing group)
						We then allow for an optional space after the optional country code
						Finally we allow for 11 digits (no spaces)
					 */
					if (!/^(?:\+\d{1,3})?\s?\d{11}$/.test(field.value) || !field.value.length) {
						errors.push(field);
					}
				},
				password: function (field) {
					/*
						The regex makes sure there is at least 8 alpha-numerical characters
						And that at least one of those values is a number
						And that at least one of those values is a text character
						We use a positive lookahead (which checks to see if a sub pattern matches a specific position)
						The lookahead checks for any character (zero or more times) is followed by a digit (e.g. making sure there is at least one digit)
						The lookahead then checks for any character (zero or more times) is followed by a text character (e.g. making sure there is at least one text character)
						Finally after the two lookaheads we have the standard regex which makes sure there is at least 8 alpha-numerical characters
					 */
					if (!/^(?=.*\d)(?=.*[a-z])\w{8,}/i.test(field.value)) {
						errors.push(field);
					}
				},
				income: function (field) { 
					if (!/^[\d,.]+/.test(field.value) || !field.value.length) {
						errors.push(field);
					}
				},
				accountnumber: function (field) {
					if (!/^\d{8}$/.test(field.value)) {
						errors.push(field);
					}
				},
				cardnumber: function (field) {
					function luhn (cardnumber) {
                        // Build an array with the digits in the card number
                        var getdigits = /\d/g;
                        var digits = [];
                        
                        while (match = getdigits.exec(cardnumber)) {
                            digits.push(parseInt(match[0], 10));
                        }
                        
                        // Run the Luhn algorithm on the array
                        var sum = 0;
                        var alt = false;
                        
                        for (var i = digits.length - 1; i >= 0; i--) {
                            // On every other number in the card sequence...
                            if (alt) {
                                digits[i] *= 2; // multiple the number by itself
                                
                                // If the number is now over 9 then we'll minus 9 from the number
                                if (digits[i] > 9) {
                                    digits[i] -= 9;
                                }
                            }
                            
                            // Add this digit onto the current total sum
                            sum += digits[i];
                            
                            // Alternate
                            alt = !alt;
                        }
                        
                        // The individual card numbers (after the above algorithm is applied and then when added together) 
                        // should be possible to divide by 10 with zero left over
                        if (sum % 10 == 0) {
                            return true;
                        } else {
                            return false;
                        }
                    }
					
					/*
						The following regex was actually borrowed from The Regular Expression Cookbook (co-written by the regex legend @stevenlevithan)
					 */
					if (!/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/g.test(field.value)) {
						errors.push(field);
					} else {
    					// If the card number appears to be valid we then run the Luhn test
    					if (!luhn(field.value)) {
        					errors.push(field);
    					}
					}
				},
				shortdate: function (field) {
					/*
						The regex allows the following formats:
							00/0000
							0/0000
							00/00
							0/00
					 */
					if (!/^\d{1,2}\/\d{2,4}$/.test(field.value) || !field.value.length) {
    				    errors.push(field);
					}
				},
				year: function (field) {
					// The regex forces the year to be a 4 digit number
					if (!/^\d{4}$/.test(field.value) || !field.value.length) {
    				    errors.push(field);
					}
				},
				checkbox: function (field) {
					if (!field.checked) {
						errors.push(field);
					}
				},
				confirm: function (field, index) {
					if (field.value.length && field.value !== fields[index].value) {
						errors.push(field);
					}
				},
				is_expiry_invalid: function (field) {
					var split = field.value.split("/"),
						month = split[0],
						year = split[1],
						paydate = new Date(window.mp_paymentdate[1], window.mp_paymentdate[0]-1, window.mp_paymentdate[2]),
						expirydate = new Date("20"+year,month-1, window.mp_paymentdate[2]); // month is zero indexed

					// We construct a date object based on the user's final pay date and their card's expiry date
					// We then convert both dates into a number so we can compare the values
					if (+expirydate <= +paydate) {
						errors.push(field);	
					}
				}
			};
			
		function display_errors(){
			// First we'll remove any messages that are already on the page
			var original_list = getElement("js-errorlist");
			if (original_list) {
				original_list.parentNode.removeChild(original_list);
			}
			
			var content,
				// We need to make sure that all mandatory fields have a custom attribute "data-errormsg"
				// Which will display the relevant message necessary if there is a problem
				err = map(errors.reverse(), function (item, index, array) {
					return {
						// I don't like the idea of having to touch the DOM every time I need to get an error message but
						// this seemed like the best way to temporarily access that data without using expando properties
						// which can cause memory leaks in Internet Explorer
						error: item.getAttribute("data-errormsg")
					}
				});
				
			function generate_html(){
				var frag = doc.createDocumentFragment(),
					div = doc.createElement("div");
					div.id = "js-errorlist";
				
				// Insert 'confirmation' header into page above the Flash file
				div.innerHTML = content;
				frag.appendChild(div);
				insert_errors.parentNode.insertBefore(frag, insert_errors);
				window.location.hash = "js-errorlist";
			}
			
			// The following code prevents calling the server every time the form is submitted.
			// Instead we retrieve the template and compile it once
			if (template) {
				content = template.render({ 
					errors: err
				});
				generate_html();
			} else {
				ajax({
					url: "Assets/Templates/FormErrors.tpl",
					data: "html",
					onSuccess: function (data) {
						template = hogan.compile(data);
						
						content = template.render({ 
							errors: err 
						});
												
						generate_html();
					}
				});
			}
		}
		
		form.onsubmit = function(){
			var len = fields.length,
				field,
				classname,
				i;
			
			errors = [];
			
			// If the "Are you already a member" module still visible then hide it.
			// No point in still showing it when we know they are trying to register
			// Also it makes it less confusing for the user if we need to display an error message box,
			// otherwise there would be two red boxes next to each other and the errors could be lost
			// This element only appears on the Stage 1 page so we need to check that it exists and if it does then we check it's class attribute value
			if (module && module.className.indexOf(" hide") === -1) {
				module.className += " hide";
			}
			
			while(len--) {
				field = fields[len];
				classname = field.className;
				i = classname.indexOf("mandatory");
				
				if (i >= 0) {
					// For the validation script to work we need to ensure a specific format is used in the HTML.
					// The main principle is to make sure the last class on an input is either 'mandatory' or 'mandatory-xxxx' 
					// e.g. 'mandatory-number', 'mandatory-dob', 'mandatory-email', 'mandatory-mobile', 'mandatory-password'
					
					if (classname.split("mandatory-")[1]) {
						// If there is a second item then we know there is a specific type to validate against
						formats[classname.split("mandatory-")[1]](field);
					} else {
						// Otherwise we validate by standard method
						// e.g. if the length is zero (meaning the field is empty) then zero will coerce to false
						// 		so we return the opposite of that using the ! operator (so the first part of the following condition is met)
						// 		and if the length is greater than zero then the regex checks to see if the content isn't just empty spaces
						if (/^\s+$/.test(field.value) || !field.value.length) {
							errors.push(field);
						}
					}
				}
				
				if (formname === "createaccount-stage1" && field.name === "confirmemail") {
					// If the email and confirm email values aren't the same then store as error
					formats.confirm(field, len-1);
				}
				
				if (formname === "createaccount-stage2" && field.name === "password2") {
					// If the password and confirm password values aren't the same then store as error
					formats.confirm(field, len-1);
				}
				
				if (formname === "createaccount-stage2" && field.name === "question2") {
					// If the 2nd question selected is the same as the 1st question field then store as error (we don't want user selecting same question twice)
					if (field.value.length && field.value === fields[len-2].value) {
						errors.push(field);
					}
				}
				
				if (formname === "createaccount-stage4" && field.name === "expirydate") {
					if (field.value.length) {
						formats.is_expiry_invalid(field)
					}
				}
			}
			
			// Make sure there are no errors stored
			// If errors.length is zero then that means there are no errors
			// Zero will coerce to false and so we return the opposite of false using the ! operator
			if (!errors.length) {
				return true;
			} 
			
			// We return false to prevent the form from submitting
			// And we display the errors we found
			else {
				display_errors();
				return false;
			}
		}
	}
	
	return validate;
	
});