Using jQuery to add form fields
A reader on another post asked me about using jQuery to dynamically add form fields to an existing form. I whipped up a quick demo that I'd like to get folks opinions on. Here is what I came up with:
2 <cfdump var="#form#">
3</cfif>
4
5<html>
6
7<head>
8<script src="/jquery/jquery.js"></script>
9<script>
10var current = 1;
11
12function addPerson() {
13 console.log('running addPerson')
14 //current keeps track of how many people we have.
15 current++;
16 var strToAdd = '<p><label for="firstname"'+current+'">Name</label> <em>*</em><input id="firstname'+current+'" name="firstname'+current+'" size="25" /> <input id="lastname'+current+'" name="lastname'+current+'" size="25" />'
17 strToAdd += '<p><label for="email'+current+'">Email</label> <em>*</em><input id="email'+current+'" name="email'+current+'" size="25" /></p>'
18 console.log(strToAdd)
19 $('#mainField').append(strToAdd)
20}
21
22$(document).ready(function(){
23 $('#addPerson').click(addPerson)
24});
25</script>
26</head>
27
28<body>
29
30<form id="someform" method="post">
31 <fieldset id="mainField">
32 <p>
33 <label for="firstname1">Name</label>
34 <em>*</em><input id="firstname1" name="firstname1" size="25" /> <input id="lastname1" name="lastname1" size="25" />
35 </p>
36 <p>
37 <label for="email1">Email</label>
38 <em>*</em><input id="email1" name="email1" size="25" />
39 </p>
40 </fieldset>
41
42 <p>
43 <input type="button" id="addPerson" value="Add Another Person">
44 </p>
45
46 <input type="submit" value="Save">
47</form>
48
49</body>
50</html>
I want to talk about this from the bottom up, so please read 'up' with me. The form contains one block of detail for a person - a firstname, lastname, and email address. There are two buttons - one to add another person and one to submit. This is what I started off with as the base form. I wanted it so that when you clicked Add Another Person, it would essentially duplicate the 3 fields.
I began by telling jQuery to monitor the click event for the button (inobtrusive JS FTW):
2 $('#addPerson').click(addPerson)
3});
addPerson then had to do two things. First, it needed to know how many people existed on the page already. I had created a JavaScript variable, 'current', with a hard coded value of 1. This doesn't need to be hard coded, but it certainly seemed to be the simplest way to handle it. Inside of addPerson, I immediately increase the value by one. I generate the HTML I want to use which was basically a cut and paste of the HTML below. The only difference is that I make the number part dynamic. I could have probably used a special character like $N and then used Regex to replace it. (On second though, that would have been a lot cleaner I think.) Then I just append the new string to the HTML.
You can view this here.
So a few open questions/things to discuss:
1) jQuery provides a clone() function for the DOM. In theory, I could have just cloned my fieldset. The problem with that (as far as I know) is that I'd end up with form fields that have the same name. That "works" in CF, but the values would be a list. So for example, form.firstname would be "Ray,Jay". That works well until someone has a comma in their name. Not very likely, but still. I believe in PHP it actually gives you the values in an array, but in CF we have no control over that. Maybe I could have done a clone, gotten the new node, and did the regex on the HTML?
2) I didn't demonstrate the CF side to parse this because I've done so many times before, but in case folks are curious - you would simply introspect the Form struct to figure out how many people you have to process.
3) Of course, the next step is to add validation. I'm willing to bet I can use the kick butt jQuery Validation plugin with dynamic forms. I'll check that next!

That being said, I have checked out jQuery based solely on the exuberance you have displayed about it in your blog posts.
I am amazed at its versatility! While I can't say its exactly _easy_ to use, the jQuery mailing list is EXTREMELY responsive and helpful, and I can see us using this in our site in the very near future.
We are still using CF7 (it works for us, and mgmt feels if it ain't broke....), and are using AjaxCFC for our AJAX calls, and jQuery makes using Rob's package SO much easier.
So, thanks for pointing us in the right direction. Again.
And fix your test page :)
@Kyle: It seems like for my use here, adding about 3 form fields, labels, etc, that adding by createElement would be somewhat slow (not slow to run, slow to write)
@Brian - do you know if clone will return the 'package'? Ie, I clone the fieldSet and get back X, X being the copy of the fieldSet so I could then dig into it and make the changes. Does that make sense?
This hidden fieldset can be outside the form tags so you don't have to worry about it when you post.
var newset = $("#hiddenFieldset").clone();
$("input[name=firstname]", newset).attr("name", "firstname"+current);
etc.
var template = new Template('<input type="text" name="field#{number}" value="" /><br /');
And then to add it you would call:
new Insertion(
'bottom': template.evaluate({number: currentCount++});
);
But Josh gave you pretty much the same answers I would have. I also tend to use a blank, hidden "template" element to use with the clone() function, then alter the newly cloned element as needed.
Joern's Validation plugin includes a template utility that will replace {n} placeholders with arguments. This demo makes use of it: http://jquery.bassistance.de/validate/demo/dynamic...
@Emilie: Interesting. I used that in one of my demos. I'm going to avoid that for now though - I want to do this stuff in baby steps. So my first step is to improve the process w/ clone. Then I'll add the validation. Of course, if I clone, I want need the templating stuff anyway since I'll just use DOM manipulation on the new node.
Of course... now that I think about it - I will have to do about 6 DOM calls (changing IDs, NAMEs), so it may make sense to NOT use DOM manipulation. Templating may be quicker. But that has the side effect of me needing to update the string if I change the main 'package' I have to clone. Then again, that would also apply to DOM as well.
Confusing! Ok, I'll take it easy at first and try clone next.
http://www.jonhartmann.com/index.cfm/2009/2/19/Exa...
http://bassistance.de/jquery-plugins/
I wrote it such that you don't have to update each "for", "name", and "id" attribute individually. You can see it/view the code for it here:
http://www.swartzfager.org/blog/demoFiles/clonedFi...
That might help dispel some of the confusion about my example.
If the form submission was a regular submission and not an AJAX submission, I suppose you could also use the jQuery remove() function on the template after the client-side validation took place and just before the form was submitted, thereby eliminating it from the form submission.
2. how we Limit the textboxes to 10. mean they should not exced more than 10. a Popup alert message should be displayed that no more than 10.
#1 You would modify the HTML added to include the link. That link would run JS code to remove the item. It would be a bit more complex, a bit too much for a blog comment.
I was tweaking around with the code a bit, trying to get it to work with something Im working on and for some reason I can't get it to work in IE. Can anyone point out why?
http://pastebin.com/m5c9478a1
As for the button - not sure. I'd have to see it to help.
http://docs.jquery.com/Events/live
$("#setcount").val(current)
That works great - one problem though - I tried adding it to the removeSet() function to allow users to remove sets they don't need, and it does not decrement the value - I'm still learning JQuery - how would I get a current count of sets created so when I POST the form, I pass on an accurate count? Thanks again for the useful information!
var current= 1;
$(document).ready(function() {
$("#addUpload").click(function() {
current++;
$newUpload= $("#userTemplate").clone(true).removeAttr("id").attr("id", "fieldSet" + current).insertBefore("#userTemplate");
//$newUpload.children("label").each(function(i) {
// var $currentElem= $(this);
// $currentElem.attr("for",$currentElem.attr("for")+current);
// });
$newUpload.children("label").children("input").each(function(i) {
var $currentElem= $(this);
$currentElem.attr("name",$currentElem.attr("name")+current);
$currentElem.attr("id",$currentElem.attr("id")+current);
});
$newUpload.children("label").children("select").each(function(i) {
var $currentElem= $(this);
$currentElem.attr("name",$currentElem.attr("name")+current);
$currentElem.attr("id",$currentElem.attr("id")+current);
});
var f = $("#fieldSet"+current);
f.html(f.html().replace("fieldSetID", "fieldSet"+current));
$newUpload.appendTo("#mainField");
$newUpload.removeClass("hideElement");
$("#setcount").val(current);
//add validation
//$("#firstname"+current).rules("add", { required:true,minlength:2 });
// $("#lastname"+current).rules("add", { required:true,minlength:2 });
// $("#email"+current).rules("add", { required:true,email:true });
//
});
//$("#uploadFields").validate({
// groups: {
// fullname: "firstname1 lastname1"
// },
// errorPlacement: function(error, element) {
// if (element.attr("name") == "firstname1"
// || element.attr("name") == "lastname1" )
// error.insertAfter("#lastname1");
// else
// error.insertAfter(element);
// },
// rules: {
// firstname1: {
// required: true,
// minlength: 2
// }
// ,lastname1: {
// required: true,
// minlength: 2
// }
// ,email1: {
// required: true,
// email: true
// }
// }
// });
});
function removeSet(id) {
$(id).remove();
$("#setcount").val(current-1);
};
Don't mind the commented out stuff - I'm going to set up the validation once I get the form submitting properly (that's a whole 'nother ball of wax), and this counter being accurate is key to that for the server-side stuff.
Oh.... wait. I don't think this code will work right. If you have 10 items and remove 2, current is always going to be 10.
You need a better solution - one that counts the items. Since each item is wrapped in a fieldset, can you give them all one class, and then simply do a $(".theclass").size()? THat code isn't exactly right - I forget how you count the results of a jQuery selector, but you get the idea I hope. You would use the same code both in the add and the subtraction.
Ah - making the field visible helped - it DOES decrement the number on removal, but ONLY the first time - subsequent removals of other sets does not decrement it. Changing the val(current-1); to val(current--) produces very strange behavior - it will ignore the first removal, but then WILL decrement subsequent removals, but will always be 1 more than the number of sets actuall left (add to this there is always an initial set that cannot be removed). Any ideas?
x--
means: Return the current value of X, THEN make it 1 smaller. You want:
--x
However, that won't work because the add code assumes current always grows bigger. If it goes down and you add to it, things will break.
See the end of my last comment.
Your suggestion worked! there was a DIV being wrapped around each set being created, and all that was required was appending the class to all the DIVs created, as well as making a DIV around the initial set with the same class so the add/remove count was accurate. Here's the bits of code that ended up working:
var n = $(".set").size();
$("#setcount").val(n);
This added to the removeSet() function as well decrements it accurately. Thanks so much for your help on this, and hopefully others can benefit from it as well!
It's me again - thanks again for your help in getting my particular issue sorted out back in March, but now I've been tasked to "enhance" this further, and one thing the users want is the last-created set of fields to have their values set to the previously-created set, so they only need to change what's needed, rather than having to duplicate effort for each set of fields each time. I've looked and looked, but couldn't find any examples of where elements cloned could have their values set to a particular set of elements. Any advice/help is appreciated!
$("#name9").val()
and then use the result to set the value in 10.
$newUpload.children("label").children("input").each(function(i) {
var $currentElem= $(this);
$currentElem.attr("name",$currentElem.attr("name")+current);
$currentElem.attr("id",$currentElem.attr("id")+current);
});
Could I use "current" in place of the ID in your example?
Thanks again for the quick reply and assistance!
OK, looks like I *don't* understand what you mean - I tried this syntax out and no dice... I wish I could wrap my head around jQuery as easily as some seem to do...
var current= 1;
$(document).ready(function() {
$("#addUpload").click(function() {
current++;
$newUpload= $("#userTemplate").clone(true).removeAttr("id").attr("id", "fieldSet" + current).insertBefore("#userTemplate");
$newUpload.children("label").children("input").each(function(i) {
var $currentElem= $(this);
$currentElem.attr("name",$currentElem.attr("name")+current);
$currentElem.attr("id",$currentElem.attr("id")+current);
$currentElem.attr("value",$currentElem.attr("id"current-1).val()+current);
});
$newUpload.children("label").children("select").each(function(i) {
var $currentElem= $(this);
$currentElem.attr("name",$currentElem.attr("name")+current);
$currentElem.attr("id",$currentElem.attr("id")+current);
$currentElem.attr("value",$currentElem.attr("id"current-1).val()+current);
});
var f = $("#fieldSet"+current);
f.html(f.html().replace("fieldSetID", "fieldSet"+current));
$newUpload.appendTo("#mainField").fadeIn(600);
$newUpload.addClass("set");
$newUpload.removeClass("hideElement");
var n = $(".set").size();
$("#setcount").val(n);
$('#docDate'+current).dateEntry();
//add validation
//$("#docType"+current).rules("add", { required:true,minlength:1 });
$("#xDocUpload"+current).rules("add", { required:true,minlength:1 });
$("#docDate"+current).rules("add", { required:true,date:true });
$("#dateCode"+current).rules("add", { required:true,minlength:1});
$("#sdoc"+current).rules("add", { required:true,minlength:1});
});
$("#uploadFields").validate({
// groups: {
// fullname: "firstname1 lastname1"
// },
errorPlacement: function(error, element) {
//if (element.attr("name") == "firstname1"
// || element.attr("name") == "lastname1" )
// error.insertAfter("#lastname1");
// else
error.insertAfter(element);
},
rules: {
//docType1: {
// required: true,
// minlength: 1
// }
xDocUpload1: {
required: true,
minlength: 1
}
,docDate1: {
required:true,
date:true
}
,dateCode1: {
required: true,
minlength: 1
}
,sdoc1: {
required: true,
minlength: 1
}
,MasterDocTypeID: {
required: true,
minlength: 1
}
}
});
});
function removeSet(id) {
$(id).fadeOut(600,function(){$(id).remove();});
var n = $(".set").size();
$("#setcount").val(n);
};
BTW, that's the whole script from my page, for the sake of completeness. I tried looking up the "other ways" I could do it (I looked into using the :last selector, but none of my experiments took me any closer to a solution). Is it Friday yet?
I think this is part of your problem:
$currentElem.attr("value",$currentElem.attr("id"current-1).val()+current);
specifically:
.attr("id"current-1)
That's not valid JS syntax. Try
.attr("id"+(current-1))
http://www.ryscript.co.cc/jQuery/jQuery-duplicate-...
thanks
Rys: Duplicating fields is not the issue (the initial example Raymond shows in this posting does an excellent job of that) - what I'm wanting is for the newest set of form fields DATA to be set based on the last set of form field's DATA that has been set by the user, so if they are doing similar (or the same) values on the fields in multiple sets, it saves them time by not having to set the same value over and over for each set they create.
Raymond: Still not working (the syntax correction you provided DOES work, but it's still not setting the newly created set's field values to the last created set's values). Since the new sets are being created from a hidden "template", does the trigger (for lack of a better term) need to be on each newly created upload set and target the "template", so when the "template" is used, the default values are the same as the last set values of the last-created set? I appreciate the help on this, as this is driving me batty trying to figure out the right way to do this.
Here's the link to the PasteBin page for the updated code:
http://pastebin.com/RktDsJxF
HUGE thanks for Raymond for the original concept/implementation, and everyone else who offered suggestions/ideas!
<input type=text" value= "water">
<input type=text" value= "soda">
<input type=text" value= "juice">
that when clicked, are added to the list and then I can just submit them all? I keep thinking this should be so simple but for some reason I can't get it....help - please.
First, my cloning is working great thanks to this post. I don't need help with that. Instead, how would you recommend handling of database updates/inserts?
My page is looping over data and presenting the data as input fields so the user can make changes and then save the changes at one time. The user needs to be able to add records, too. Let's say the form has 10 rows with each row having 5 fields to update and the new needs to add 3 new records (5 fields per row). I can update the 10 records, no problem, but how do I account for the 3 newly added records of data?
Note: the 10 rows have a unique id value but they are not necessarily sequential.
http://pastebin.com/nEDhfcdB
Keep in mind it's pretty specific to what I need to do, but the looping and discovery of the dynamically-numerated fields coming in code should give you ideas on how to approach your needs. Good luck!
For example I create an element with name=ex2 by pressing the button. After form submit $_POST['ex2'] does not exist...
What am I doing wrong? How can I get the value of the dinamicaly created ements after the form is submitted?
I have tried this:
function removePerson() {
$('#mainField').remove('#form'+current)
}
$(document).ready(function(){
$('#removePerson').click(removePerson)
});
but it does not work. any help please? thanks
First, ensure your click handler is actually running. Add a console.log("Yo!") as the first line in removePerson().
Then - you need to ensure your remove is working right. You are telling it to remove #formX (where X == current) from within mainField. So perhaps double check that #formX actually matches something:
var test = $("#form"+current);
console.log(test.length);
I will have another look at my code and see if removePerson is actually doing something. thanks.
Here is the full code if it helps thanks.
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1...;
<script>
var current = 1;
function addPerson() {
current++;
var strToAdd ='<h2 id="form'+current+'">Attendee'+current+'</h2><table><tr><td><label>First Name</label></td><td><input id="firstname'+current+'" name="firstname'+current+'" type="text" /></td><td>Phone:</td><td><input id="phone'+current+'" name="phone'+current+'" type="text" /></td></tr><tr><td>Last Name:</td><td><input id="lastname'+current+'" name="lastname'+current+'" type="text" /></td><td>Company:</td><td><input id="company'+current+'" name="company'+current+'" type="text" /></td></tr><tr><td>Email:</td><td><input id="email'+current+'" name="email'+current+'" type="text" /></td></tr></table>'
$('#mainField').append(strToAdd)
} //end add
function removePerson() {
// alert("yo!");
$('#mainField').remove('#form '+current+)
}
$(document).ready(function(){
$('#addPerson').click(addPerson)
});
$(document).ready(function(){
$('#removePerson').click(removePerson)
});
var test = $("#form"+current);
alert(test.length);
</script>
</head>
<body>
<form id="someform" method="post" action="">
<fieldset id="mainField">
<h2>Attendee</h2>
<table>
<tr>
<td>First Name</td>
<td><input id="firstname1" name="firstname1" type="text" /></td>
<td>Phone:</td>
<td><input id="phone1" name="phone1" type="text" /></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input id="lastname1" name="lastname1" type="text" /></td>
<td>Company:</td>
<td><input id="company1" name="company1" type="text" /></td>
</tr>
<tr>
<td>Email:</td>
<td><input id="email1" name="email1" type="text" /></td>
</tr>
</table>
</fieldset>
<p>
<input type="button" id="addPerson" value="Add Another Person">
<input type="button" id="removePerson" value="Remove Person">
</p>
<input type="submit" value="Post">
</form>
</body>
</html>
First off, this looks wrong: $('#mainField').remove('#form '+current+)
Notice the + at the end. That's not valid JavaScript.
Secondly, you put those 2 test statements by themselves. They should be after the alert('yo')