Here is an interesting thing I played with this Sunday morning. How can I create a dynamic form on the client side and combine it with dynamic processing on the server side? For example - consider a form that lets you send email to your friends. By default we could ask you for 5 sets of names and email addresses. But what if you aren't a programmer and have more than five friends? You could use a form that lets you enter data and includes a button labeled "I have more friends." The form would post to itself and then simply add more blank fields. That's kind of what Soundings does. It works, but we can do better, right?
Let's start with a simple form and a set number of fields.
<!--- Number of default rows to show --->
<cfset defaultRows = 5>
<html>
<head>
</head>
<body>
<p>
Enter the names and email addresses of all your friends so we can spam them.
</p>
<form action="index.cfm" method="post" id="mainform">
<table id="maintable">
<tr>
<th>Name</th>
<th>Email</th>
</tr>
<cfloop index="x" from="1" to="#defaultRows#">
<tr>
<cfoutput>
<td><input type="text" name="name#x#" class="name"></td>
<td><input type="text" name="email#x#" class="email"></td>
</cfoutput>
</tr>
</cfloop>
</table>
</body>
</html>
<p>
Enter a message for them:<br/>
<textarea name="msg" cols="40" rows="6"></textarea>
</p>
<input type="submit" value="Send">
</form>
Nothing too complex here. I've set a CF variable, defaultRows, that indicates I'll have 5 sets of friends. Note then the loop that creates one TR for each set.
This works well enough, but how can we add another set so we can add more friends? I looked to jQuery to see how easy it would be to add a new table row. I seem to remember that adding content, in general, to the browser is easy, but that tables were a bit more difficult. I figured, though, that jQuery would make this easier.
A quick Google search turned up a solution by a user named motob (http://www.mail-archive.com/jquery-en@googlegroups.com/msg20739.html). I've no idea who he is, but thanks to him, I was able to get it working. I began by adding this link below the table:
<a href="" onclick="addrow();return false;">Add Friend</a>
And then added the following code:
<script src="/jquery/jquery.js"></script>
<script>
var totalRows = <cfoutput>#defaultRows#</cfoutput>;
function addrow() {
totalRows++;
var clonedRow = $("table#maintable tr:last").clone(); //this will grab the last table row.
//get the textfield
var tfName = $("input.name",clonedRow);
var tfEmail = $("input.email",clonedRow);
//change their names
tfName.attr("name","name"+totalRows);
tfEmail.attr("name","email"+totalRows);
$("table#maintable").append(clonedRow); //add the row back to the table
console.log('done')
}
</script>
I begin by loading in my jQuery library. Kind of hard to use jQuery without it. Next I create a page-wide variable, totalRows, that matches the ColdFusion variable defaultRows. Next up is addrow. This is where the magic is. I use the clone() function as described by motob. This creates a clone of the last table row. Note - when I first tried this code my 'Add Friend' link was in a right-aligned final row. I had to work around that a bit and then decided to just make it simpler and remove the link from the last row.
Ok, so the clone works just fine, but we need to get the input controls from within. If you look back at, you will notice I set a class for each input field. This doesn't match to any real CSS, but is just used as a marker. (Is there a better way, jQuery gurus?) Once I have a pointer to each input field, it is a simple matter to update their names. Lastly, I append the row back to the table.
A bit off topic, but what's the deal with console.log() at the end? This will only work in Firebug, so IE users please remove it. I added this in to make it easier to see if the code worked. When you have a link/button/whatever run JS code and the JS code has an error, the page will simply let the link 'continue' and the page will reload. Now Firefox does have a nice error console and I keep it open, but sometimes the reload 'flash' is so quick I miss it. By using that conole.log at the end, I get a quick way to see if everything ran ok, or at least did not throw an error.
Ok, so that's the client side, on the server side, it isn't that complex. I added this to the top of my page:
<cfif not structIsEmpty(form)>
<cfset counter = 1>
<cfloop condition="structKeyExists(form,'name#counter#') and structKeyExists(form,'email#counter#')">
<cfset name = form["name#counter#"]>
<cfset email = form["email#counter#"]>
<cfif len(trim(name)) and isValid("email", email)>
<cfmail to="#email#" from="ray@camdenfamily.com" subject="A very special message....">
Hi #name#!
#form.msg#
</cfmail>
<cfoutput>I sent email to #name# (#email#)<br/></cfoutput>
</cfif>
<cfset counter++>
</cfloop>
</cfif>
I simply loop with a condition checking to see if nameX and emailX exists. I'll continue onto infinity so for those of you with more than 5 friends, you will be covered. (If I did this during high school I'd have to add a 'Remove Friend' function!)
For each loop I grab the values, do basic validation, and send email. (I should also add basic validation to form.msg as well.) The complete code is below. Like always, I'll remind people I'm still new to jQuery so most likely it could be done a bit better.
<cfif not structIsEmpty(form)>
<cfset counter = 1>
<cfloop condition="structKeyExists(form,'name#counter#') and structKeyExists(form,'email#counter#')">
<cfset name = form["name#counter#"]>
<cfset email = form["email#counter#"]>
<cfif len(trim(name)) and isValid("email", email)>
<cfmail to="#email#" from="ray@camdenfamily.com" subject="A very special message....">
Hi #name#!
#form.msg#
</cfmail>
<cfoutput>I sent email to #name# (#email#)<br/></cfoutput>
</cfif>
<cfset counter++>
</cfloop>
</cfif>
<!--- Number of default rows to show --->
<cfset defaultRows = 5>
<html>
<head>
<script src="/jquery/jquery.js"></script>
<script>
var totalRows = <cfoutput>#defaultRows#</cfoutput>;
function addrow() {
totalRows++;
var clonedRow = $("table#maintable tr:last").clone(); //this will grab the last table row.
//get the textfield
var tfName = $("input.name",clonedRow);
var tfEmail = $("input.email",clonedRow);
//change their names
tfName.attr("name","name"+totalRows);
tfEmail.attr("name","email"+totalRows);
$("table#maintable").append(clonedRow); //add the row back to the table
console.log('done')
}
</script>
</head>
<body>
<p>
Enter the names and email addresses of all your friends so we can spam them.
</p>
<form action="index.cfm" method="post" id="mainform">
<table id="maintable">
<tr>
<th>Name</th>
<th>Email</th>
</tr>
<cfloop index="x" from="1" to="#defaultRows#">
<tr>
<cfoutput>
<td><input type="text" name="name#x#" class="name"></td>
<td><input type="text" name="email#x#" class="email"></td>
</cfoutput>
</tr>
</cfloop>
</table>
<a href="" onclick="addrow();return false;">Add Friend</a>
</body>
</html>
<p>
Enter a message for them:<br/>
<textarea name="msg" cols="40" rows="6"></textarea>
</p>
<input type="submit" value="Send">
</form>
Archived Comments
"But what if you aren't a programmer and have more than five friends?"
Careful Ray, I'm still drinking my coffee and almost had to clean up a mess :)
Also remember that, like checkboxes, multiple fields of the same name will do a comma delimited list on the server side.
<input name="email" value="1@1.com" />
<input name="email" value="2@2.com" />
Result:
form.email = "1@1.com,2@2.com" />
- WB
@WillB: True, but I didn't want to have to worry about parsing lists and handling empty results.
Actually that would be impossible to use. If you have 3 fields named the same and put stuff in 1 and 3, you get
form.name=a,c
No empty item. So you would not be able to correctly 'sync' up randomly empty items from both a name list and an email list.
"But what if you aren't a programmer and have more than five friends?"
Brilliant!
@Ray Good point. I was thinking about our own send-to-friend forms. We only ask for the email, not the name, so yeah -- that would be an issue.
Tip: set the name to xx.y1, and xx.y2 to get the struct FORM.xx :)
So in your example, you can set the name to be:
entries.1.name, and
entries.1.email
Then do a cfloop on FORM.entries (of type struct). :)
@Henry
I do something similar using CSS and JS to make new form entries. Then i use the dreaded evaluate function to get the values on the action page.
I use CSS classes that don't necessarily map to a CSS style all the time with jQuery as markers. I don't see anything wrong with it really, since your ID must be unique and it is an easy way to group related items.
I suppose you could do something like:
$("input[name^='name']")
To find all text inputs that begin with 'name'. See docs for more info:
http://docs.jquery.com/Sele...
@Henry (Yes, I replied to myself)
I just found out there's a catch, bind expression will fail if name attribute contains '.'
@Henry (yes, I reply to myself again!)
I thought CF would do that automatically... It doesn't.
Here's the script that turns it into struct.
http://www.bennadel.com/blo...
sorry for the incorrect tip~ :)
Also note that Brian Kotek has a project that handles implicit structure (as well as array) creation via this technique:
http://formutils.riaforge.org/
@Chad,
If you treat your form like a struct (as Henry mentioned) you don't need to use evaluate. I use a hidden field to know how many new fields have been created; javascript updates it. Say your form has this:
<input type="hidden" name="newFieldCount" value="2">
<input type="text" name="myNewField_1" value="foo"> <!-- created by javascript, so "1" appended dynamically -->
Then, server side you have this:
<cfloop from="1" to="#form.newFieldCount#" index="i">
<cfset myNewField = form["myNewField_#newFieldCount#"]>
</cfloop>
So funny. Remove friend button! In case 10 minutes elapse and James flirts with Kim again while Melissa is in the restroom.
I'm guessing there's an equivalent in JQuery, but in Ext I would've foregone the arbitrary CSS classing of the input elements and once I had the new table row used its sub-selection method (Ext.Element.select) to return its input elements. Then you just change them and set. Less markup goes to the browser, and I'm assuming that querying an element for its children of a certain type is at least trivially faster than querying all of its children for matching items of a desired class.
Great post.