Cold Fusion in Context: Bulk Data Entry and E-mail Validation

Suppose you wanted to let someone enter an unlimited number of license numbers, or E-mail addresses, or phone numbers into your form. Suppose you wanted to let someone paste a column from an Excel spreadsheet or address book into your form. Suppose you wanted to validate the result before letting the user submit the form. This tip will let you do all three.

You've used the areatext tag for raw text before, but Ella Furlong suggests using it as a column with unlimited rows. When you do this, you don't have to guess how many entries your user would like to make at one time, and the user doesn't have to press the equivalent of "continue" to feed you more entries if you've guessed wrong. An additional benefit of this technique is that the user can paste multiple entries into the field at once.

Finally, giving users feedback about their entries before pressing the submit button saves them time, saves bandwidth, and keeps the load off of your server. Validating E-mail address formats is a common request; so, this demo does that. Call this code getMany.cfm.

Validate the Format of an E-mail Address

Checking data on the client side is easily done with javascript and regular expressions. Begin by opening the script and hiding its contents from old browsers with an HTML comment. Give this function two parameters: the short name for the field to be checked, and its name on the screen. Construct the formal name for the field by using the eval function to turn text consisting of this field's place in the document hierarchy [document.form_name.field_name] into an object you can work with.

<script language="javascript">
<!--
// Confirms correct E-mail addresss format.
function mailcheck(myField, myName)
{
fullField = eval('document.demo.'+myField);

The replace function seeks a regular expression in a value and replaces things that match this expression. The up-carat matches the beginning of a string. Adding an asterisk to a group says you want to match zero or more occurences of the group; the group backslash-s matches any white space (tabs, spaces, etc.). Therefore, the first replace statement trims leading white space. The dollar sign matches the end of a string. Therefore, the second replace statement trims trailing white space. If you're left with nothing when this is done, the function exits successfully; there's no bad address to complain about.

fullField.value = fullField.value.replace(/^\s*/, '');
fullField.value = fullField.value.replace(/\s*$/, '');
if (fullField.value.length < 1) return true;

Now because this field can contain many addresses, let's parse it. First convert all white space (g for globally, i for case-insensitive) to spaces. Then assign the values that aren't spaces to elements in an array by using the split function. Finally, begin to check each element. Just before each barrage of checks, add 1 to the element number (which starts at zero) to yield an address number (starting with 1) in case you need to tell the user which address you didn't like.

mySep = /[\s]+/gi;
myText = fullField.value.replace(mySep, ' ');
mySplit = myText.split(' ');
nr = 0;
for (i = 0; i < mySplit.length; i++)
  {
  nr = i+1;

A proper E-mail address has a specific format. It consists of chunks of mostly letters and numbers with an occasional underline or hyphen that doesn't touch the outside of the chunk. Because this is not a simple concept, it makes sense to define a chunk as a single character to hide this complexity. This text uses a percent sign, something that shouldn't be in an E-mail address anyway, as that character. So, step one is to complain if that character is in the address. The match function sees if it can find a regular expression in a string.

  if (mySplit[i].match(/%/))
    {
    alert(myName+' '+nr+' has %');
    return false;
    }

The address must end in a dot followed by two to four characters. The i gives you a case-insensitive search; the dollar sign says what you're looking for must touch the end of the string. It's looking for two to four characters in the range from a to z. That whole mess must start with a dot. Because a period has a special meaning in regular expressions, it's preceded with a backslash to take that special meaning away. The match is testing element i. Finally, the exclamation point negates the function; so, the "if" is satisfied when the match does NOT occur (i.e., the desired suffix is MISSING).

  if (!mySplit[i].match(/\.[a-z]{2,4}$/i))
    {
    alert(myName+' '+nr+' has a bad suffix');
    return false;
    }

At long last, here's the main event. Use g and i to make this a global, case-insensitive search. Define a match as one or more occurrences of numbers and/or letters. Alternatively, define a match as two groups of numbers and/or letters surrounding hyphens and/or underscores. Then, replace each match with a percent sign [%].

Now make sure that the address:
- begins with zero or more occurrences of [%.],
- next has a % followed by an "at" sign,
- next has zero or more occurrences of [%.],
- and ends in [%.%].

Finally, close the for loop and the function.

  chunk = /([0-9a-z]+[-|_]+[0-9a-z]+)|[0-9a-z]+/gi;
  mySplit[i] = mySplit[i].replace(chunk, "%");
  // uncomment to understand // alert(mySplit[i]);
  if (!mySplit[i].match(/^(%\.)*%+@{1}(%\.)*(%+\.%)+$/))
    {
    alert(myName+' '+nr+' has the wrong format');
    return false;
    }
  }
return true;
}

Consider Submitting the Form

To work interactively, the mailcheck function is invoked when the user changes and then leaves a field. However, this won't help if the form has a submit field and the user pushes its button instead of cleaning up problem entries. Therefore, you need to create your own function that considers whether to submit the form instead of just doing it.

The form has two input fields: doers (labeled "TO") and watchers (labeled "CC"). Therefore, consider will run mailcheck against both of these fields. If a check fails, the form doesn't get submitted. If the checks are OK, it does. The double slash is a javascript comment to hide the closing HTML comment from javascript. Closing the script tag brings us back to work on the rest of the page.

function consider()
{
if (!mailcheck('doers', 'TO')) return false;
if (!mailcheck('watchers', 'CC')) return false;
document.demo.submit();
return true;
}
//-->
</script>

Make the Entry Form

Now it's time to make the form described above. Give the form a label; set defaults for the first time it's used; and label and define the fields. The wrap attribute preserves some formatting when the form is submitted and usually supresses the horizontal scroll bar. The rows and cols attributes refer to the physical display and wrap; they don't limit the total amount of data the field can receive (which for practical purposes is unlimited). The onChange attribute invokes mailcheck with the appropriate field name and label.

The textarea tag has an opening tag, a value, and a closing tag. In order to display the value, wrap it in cfoutput tags. Close the tag.

When you've done these things for both fields (with a break in between), add a button and close the form. Notice that this is not an input submit tag; it's a button tag. Its onClick attribute calls consider to test the fields and perhaps submit the form. Close the form.

<h3>Paste E-mail Lists into Windows</h3>

<cfparam name="form.doers" default="">
<cfparam name="form.watchers" default="">
<form name="demo" action="getMany.cfm" method="post">
TO: <textarea name="doers" wrap="physical" rows="3" cols="60" 
onChange="mailcheck('doers','TO')"><cfoutput>#trim(form.Doers)#</cfoutput></textarea><br>
CC: <textarea name="watchers" wrap="physical" rows="3" cols="60" 
onChange="mailcheck('watchers','CC')"><cfoutput>#trim(form.watchers)#</cfoutput></textarea>
<input type="button" name="run" value="OK" onClick="consider()">
</form>

Prove You Can Interpret the Entries

Now that you've solicited this input, it's time to prove that you can turn it into a separate row for each address (even if it wasn't entered that way). Label the information; set up a list loop to parse it; and use cfoutput to display it. Notice carefully that there are three delimiters used here: space, linefeed, and carriage return. The delimiters drop out, leaving the data you want.

<h3>DOERS: "TO" ADDRESSES</h3>
<cfloop list="#form.doers#" index="address"
delimiters=" #chr(10)##chr(13)#">
<cfoutput>#address#</cfoutput><br>
</cfloop>
<p>
<h3>WATCHERS: "CC" ADDRESSES</h3>
<cfset nr=0>
<cfloop list="#form.watchers#" index="address"
delimiters=" #chr(10)##chr(13)#">
<cfoutput>#address#</cfoutput><br>
</cfloop>

Try It; Extend It

Paste all of this text into getMany.cfm and invoke it. Dump some E-mail addresses into it. Mangle them to see what the form does. Type a dozen or so into a text document and then paste the document into the field for bulk data entry.

Consider that the only thing left out to make this a complete cycle is getting the addresses into and out of a database. You've already proven you can confirm the format of the data on the client side and can then break it into separate addresses, one per row. You can do the row insert inside the loop that breaks them up.

You have a couple of options when you send them back to the user from the database. If you use the textarea tag for re-display and edit/delete, the simplest approach is to handle changes to individual addresses as a delete-insert combination. The other option is to bring them back the rows in a multi-line form using conventional tags where you can readily tell if the intent was to change an existing row or to delete the old value.

This general approach isn't limited to addresses, of course. Imagine letting your users paste data from documentation tools or old reports directly into your database using a Web page. Imagine being able to let users make extended entries without having to bring up yet another page to accommodate the extra data. Try this technique and tell others what you've learned. =Marty=
[This demonstration has been updated to accommodate top-level domains longer than three characters (e.g., ".info").]