Abstract Submit

From BriansWiki

Jump to: navigation, search

Here's a way to simplify the Javascript required to do an Ajax submit of any ColdFusion form. In this example we are also including an abstract form validation since that should be part of any submit function.

Contents

[edit] Define Fields

To start you need to define all the fields on your form that are required and that have data to commit:

<cfscript>
	// setup all the required fields by type: these are used by the validation routine
	lRequiredFields = 'edit_lNameofAccused,edit_fNameofAccused,edit_Address,edit_City,edit_State,edit_Zip,edit_System';
	lRequiredDateFields = 'edit_FormSubmitted';
	lRequiredNumericFields = '';
	lRequiredPhoneFields = 'edit_CallerPhone';
	
	// setup the submit fields by type
	lStringTextFields = 'edit_Rep,edit_lNameofAccused,edit_fNameofAccused,edit_CallerPhone,edit_Address,edit_City,edit_Zip';
	lNumericTextFields = 'edit_AcctNumber';
	lSelectFields = 'edit_State,edit_System,edit_Source,edit_ReportCode';
	lBitFields = 'edit_ContactYou';

	// define the Action file, type of action and the div to return to.
	processFile = 'tosDetailAction.cfm';
	processType = 'edit_';
	returnDiv = 'tosUpdateResults';	
</cfscript>

In this example we are doing a simple form submit to a database with the status of the commit operation returned to the "tosUpdateResults" div at the top of the form. By changing the "processType" property to "srch_", you can use this same function for a search page submit that returns the results to a List div.

This all assumes that you follow the best practice of naming your fields with a form-type prefix and the exact database table field name. There also is a requirement for the validation function that your input fields have a span that is named after this field with a prefix of "inf_". For example, if you have a field in your table called "Address", you would setup the form field like this:

	<tr>
		<td valign="top" class="label_cell"><label for="edit_Address">  #ListGetAt(lFormFieldsView,6)#:</label></td>
		<td>
			<input type="text" name="edit_Address" id="edit_Address" value="<cfoutput>#oForm.getAddress()#</cfoutput>" size="50" onchange="validatePresent(this, 'inf_Address');" />
		  	<span id="inf_Address" style="display: inline; color: red;">*</span>
		</td>
	</tr>

Please excuse the inline styles - they are here to let you know how we get the span to not introduce a line break and show up in red, you really should add this to your style sheet for the application.

[edit] Call Process Form on Submit

Instead of using the action page directly, we want to use a JavaScript "processForm" function that allows abstract page handling and validation all using an Ajax submit for easy page handling; no need to save your form values in session since you never leave the page to validate so you don't need to reload on an error.

Here's how to do the button event:

<input type="button" name="btnEdit_OK" value="  OK  " class="formButton" onClick="processForm('#lRequiredFields#','#lRequiredDateFields#','#lRequiredNumericFields#','#lRequiredPhoneFields#','#lDateTextFields#','#lNumericTextFields#','#lStringTextFields#','#lSelectFields#','#lBitFields#','#processFile#','#processType#','#returnDiv#'); return false;">

[edit] Process the Form

Now we are in the Javascript request handler; the goal here was to be able to make this as abstract as possible so we don't need to waste time handling explicit fields and their value types.

Here's the "processForm" function and the Ajax "fileHandler":

/* ---------------- process form ----------------------- */

	function processForm(requiredFields,requiredDateFields,requiredNumericFields,requiredPhoneFields,dateTextFields,numericTextFields,stringTextFields,selectFields,bitFields,processFile,processType,returnDiv)	{
		// requiredFields - list of all the generic required fields - they will be checked for "is present"
		// requiredDateFields - list of required date fields - checked for date validity and presence
		// requiredNumericFields - list of required numeric fields - their value has to be a number greater than 0
		// requiredPhoneFields - list of required phone fields - checked for 10 digits - dashes and parentheses are ok
		// dateTextFields - list of date input fields in the form
		// numericTextFields - list of text input fields in the form with numeric values
		// stringTextFields - list of text input fields in the form with varchar values
		// selectFields - list of select fields in the form
		// bitFields - list of checkbox / radio button fields
		// processFile - action file associated with this form
		// returnDiv - ID of div where results are displayed
		
		// add all the required fields to the url so we can do server side validation too,
		str = requiredFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			requiredFields = str2;
			fileString = fileString + "&requiredFields="+requiredFields;
		}
		str = requiredDateFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			requiredDateFields = str2;
			fileString = fileString + "&requiredDateFields="+requiredDateFields;
		}
		
		str = requiredNumericFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			requiredNumericFields = str2;
			fileString = fileString + "&requiredNumericFields="+requiredNumericFields;
		}
		
		str = requiredPhoneFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			requiredPhoneFields = str2;
			fileString = fileString + "&requiredPhoneFields="+requiredPhoneFields;
		}
		
		// now validate the form before we do anything else
		var bValid = validateForm(requiredFields,requiredDateFields,requiredNumericFields,requiredPhoneFields,processType);
		if (!bValid) {
			return false;
		}
		
		// start the filestring
		var fileString = processFile + '?';

		var str = dateTextFields;
		if (str.length != 0)	{
			var str2 = replaceString(str,'',processType);
			dateTextFields = str2;
			fileString = fileString + "&dateTextFields="+dateTextFields;
		}

		str = numericTextFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			numericTextFields = str2;
			fileString = fileString + "&numericTextFields="+numericTextFields;
		}
		
		str = stringTextFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			stringTextFields = str2;
			fileString = fileString + "&stringTextFields="+stringTextFields;
		}
		
		str = selectFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			selectFields = str2;
			fileString = fileString + "&selectFields="+selectFields;
		}
		
		str = bitFields;
		if (str.length)	{
			str2 = replaceString(str,'',processType);
			bitFields = str2;
			fileString = fileString + "&bitFields="+bitFields;
		}
		
		var IDobj = "";
		var IDindex = "";
		var IDfield = "";
		var fieldName = "";
		var IDvalue = "";
		var fieldList = "";
		var i=0;
		var newDateTextFields = "";
		var newNumericTextFields = "";
		var newStringTextFields = "";
		var newSelectFields = "";
		var newBitFields = "";

		if (dateTextFields.length > 0)	{
			fieldList = dateTextFields.split(',');
			for (i=0; i<fieldList.length; i=i+1){
				fieldName = processType+fieldList[i];
				IDobj = document.getElementById(fieldName);
				IDvalue = IDobj.value;
				fileString = fileString + "&" + fieldList[i] + "=" + IDvalue;
			}
		}
		
		if (numericTextFields.length > 0)	{
			fieldList = numericTextFields.split(',');
			for (i=0; i<fieldList.length; i=i+1){
				fieldName = processType+fieldList[i];
				IDobj = document.getElementById(fieldName);
				IDvalue = IDobj.value;
				fileString = fileString + "&" + fieldList[i] + "=" + IDvalue;
			}
		}
		
		if (stringTextFields.length > 0)	{
			fieldList = stringTextFields.split(',');
			for (i=0; i<fieldList.length; i=i+1){
				fieldName = processType+fieldList[i];
				IDobj = document.getElementById(fieldName);
				IDvalue = IDobj.value;
				fileString = fileString + "&" + fieldList[i] + "=" + IDvalue;
			}
		}
		
		if (selectFields.length > 0)	{
			fieldList = selectFields.split(',');
			for (i=0; i<fieldList.length; i=i+1){
				fieldName = processType+fieldList[i];
				IDobj = document.getElementById(fieldName);
				IDindex = IDobj.selectedIndex;
				IDvalue = IDobj.options[IDindex].value;
				fileString = fileString + "&" + fieldList[i] + "=" + IDvalue;
			}
		}
		
		if (bitFields.length > 0)	{
			fieldList = bitFields.split(',');
			for (i=0; i<fieldList.length; i=i+1){
				fieldName = processType+fieldList[i];
				IDobj = document.getElementById(fieldName);
				if (IDobj.checked == true)	{
					IDvalue = 1;
				}	else	{
					IDvalue = 0;
				}
				fileString = fileString + "&" + fieldList[i] + "=" + IDvalue;
			}
		}
		var urlFile = fileString.replace(/\'/g,"*");
		fileHandler(urlFile, returnDiv); 
	}

	
/* ---------------- file handler ----------------------- */

 	function fileHandler(detailFileName, divName)	{
		// alert('fileHandler: '+detailFileName+' | '+divName);
		http.open("GET", detailFileName, true);
		http.onreadystatechange = function()  {
			if (http.readyState == 4) { 
				if (http.status == 200) {
					 results = http.responseText; 
					 document.getElementById(divName).innerHTML = results; 
					 
					 // see if we need to setup list
					 if (divName=='tosList'){
					 	 setupList();
					 }
					 if (divName=='tosDetail'){
					 	 setupDetail();
					 }
					 if (divName=='tosUpdateResults') {
				   		 showStatus('tosUpdateResults');
					 }
				} 
				else {
					document.getElementById(divName).innerHTML = "Failed." + http.status; 
				}
			} 
			else {
				//document.getElementById(divName).innerHTML = "<b>Loading....</b>";    
			}	
		}
		http.send(null);   
	}

The "fileHandler" function is not completely abstract since we do need to do some special form "housekeeping" depending on the type of call. In this example we need to run the "sortFormat" function if we are loading a Best Table Sort Ever list and we need to display the results div and hide the list div if we are doing a detail submit. The last else is in the case of a simple submit, where we just display the status div.

[edit] Son of fileHandler

The fileHandler function detailed above was screwing up my divs so I replaced it with the Prototype Ajax.Updater. I kept the same interface as the old handler, even though it is stupid that I build the url in 'ProcessFile' and then break it apart again here. The rationale is that the code generator, Code Generation uses this syntax in lots of places so I need to go back and change all those templates. Ideally, we should just pass the url parameters as a separate variable as the Ajax.Updater method expects:

	function fileHandler(urlFile, container) {
		var pars = getURLParams(urlFile);
		var url = getURL(urlFile);
 		var ajax = new Ajax.Updater(container, url,{
			method: 'get',
			parameters: pars,
			onFailure: reportError,
			onLoading: function(){$(container).innerHTML = "<b>Loading....</b>"},
			onComplete: function(){postHandler(container)}
		});
 	}

	// Get all the parameters as a string without the filename
	function getURLParams(strUrl){
	  var strReturn = "";
	  if ( strUrl.indexOf("?") > -1 ){
	  	var pos = strUrl.indexOf("?") + 1
	    strReturn = strUrl.substr(pos);
	  }
	  return strReturn;
	}
	  
	// Get just the filename from the whole Url
	function getURL(strUrl){
	  var strReturn = strUrl;
	  if ( strUrl.indexOf("?") > -1 ){
	  	var pos = strUrl.indexOf("?")
	    strReturn = strUrl.substr(0,pos);
	  }
	  return strReturn;
	}
	
	// Alert the user about an error
	function reportError() {
		alert('Their was an error during the AJAX Update.');
	}

 	// This is where you do your app specific stuff:
	function postHandler(divName) {
 		// see if we need to setup list
		if (divName=='tosList'){
		 	 setupList();
		}
		if (divName=='tosDetail'){
		 	 setupDetail();
		}
		if (divName=='tosUpdateResults') {
	   		showStatus('tosUpdateResults');
		}
	}

Note this introduces a dependency on the Prototype library, which is included by default for all Intranet applications. If you are on another server you will need to include this file:

<script type="text/javascript" src="scripts/prototype.js"></script>

[edit] Validate the Form

Validation is also handled abstractly except that we can only check for the following types of conditions:

  • Is Present
  • Is Present and is Numeric and greater than 0
  • Is Present and is Date in "mm/dd/yyyy" format
  • Is Present and is Phone Number in "123-456-7890" or "(123)456-7890" format

If you noticed in the Process Form function, one of the first things we do is strip the "process" prefix from the field names and then run the validation, we can probably skip this step by not including the prefix to begin with but we still want to include the required fields in the url so we can do server side validation too.

This validation function references functions in the formVal.js file originally written by Stephen Poley; Here's the reference to add to your application.cfc:

	<!--- form validation Script --->
	<script language="JavaScript" src="scripts/formVal.js" type="text/javascript"></script>	

Note: Remember to Adjust the path to fit where you copied this file.

Here's the abstract Validation function:

function validateForm(requiredFields,requiredDateFields,requiredNumericFields,requiredPhoneFields,processType) {
    	var errs=0;
	var fieldName = "";
	var infoName = "";
	var fieldList = "";
	var i=0;    

    	// Check generic "is Present" fields
    	if (requiredFields.length > 0)	{
		fieldList = requiredFields.split(',');
		for (i=0; i<fieldList.length; i=i+1){
			fieldName = processType+fieldList[i];
			infoName = 'inf_'+fieldList[i];
			if (!validatePresent (document.getElementById(fieldName), infoName)) errs += 1; 			}
	}
		
	// Check for date fields
	if (requiredDateFields.length > 0)	{
		fieldList = requiredDateFields.split(',');
		for (i=0; i<fieldList.length; i=i+1){
			fieldName = processType+fieldList[i];
			infoName = 'inf_'+fieldList[i];
			if (!validateDate (document.getElementById(fieldName), infoName, true)) errs += 1; 
		}
	}
		
    	// Check for numeric fields
	if (requiredNumericFields.length > 0)	{
		fieldList = requiredNumericFields.split(',');
		for (i=0; i<fieldList.length; i=i+1){
			fieldName = processType+fieldList[i];
			infoName = 'inf_'+fieldList[i];
			if (!validateNumber (document.getElementById(fieldName), infoName, true, 1)) errs += 1; 
		}
	}
		
	// Check for phone fields
	if (requiredPhoneFields.length > 0)	{
		fieldList = requiredPhoneFields.split(',');
		for (i=0; i<fieldList.length; i=i+1){
			fieldName = processType+fieldList[i];
			infoName = 'inf_'+fieldList[i];
			if (!validateTelnr (document.getElementById(fieldName), infoName, true)) errs += 1; 
		}
	}
    	
    	// Check to see if there were any errors and alert the user if found
	if (errs>1)  alert('There are fields which need correction before sending');
	if (errs==1) alert('There is a field which needs correction before sending');

    	if (errs == 0) {
    		return true;
    	} else {
    		return false;
  		}
  	}

If the function returns true, the submit will continue, if not the fields that are invalid will be flagged and the error condition displayed in the "inf_" span on the form next to the offending field.