Ask Ben: Dynamically Adding File Upload Fields To A Form Using jQuery
So seeing as you are on a jQuery kick I have a simple code request from you. Very simple, I want to allow file uploads in a form. I want it to start with 1 file upload option displayed, and then if the user clicks on a link, another one upload is displayed below that one. I want it all to be dynamic and create the html on the fly and use dynamic data. For example:
On Load:
<div id="files">
<div id="file1" class="row">
1: <input type="file" name="presoFile1">
</div>
</div>After Click:
<div id="files">
<div id="file1" class="row">
1: <input type="file" name="presoFile1">
</div>
<div id="file2" class="row">
2: <input type="file" name="presoFile2">
</div>
</div>
One thing that jQuery makes really easy is accessing and dynamically modifying the DOM. Sure, the animation stuff is cool, no doubt; but, to me, the real power of jQuery - the secret sauce - is the ability to painlessly mess with the document object model (DOM). Now, when it comes to adding completely new elements to the DOM, there are a number of ways to do it. As the target element that you are creating gets more complicated, I have found that one easy way to work with it is to create a hidden template of it somewhere on the page (or to load via AJAX).
For example, using your question above, I might have an "element-templates" DIV that looks like this:
<!--- DOM templates. --->
<div id="element-templates" style="display: none ;">
<div id="::FIELD1::" class="row">
::FIELD2::: <input type="file" name="::FIELD3::">
</div>
</div>
Using this template, you can easily begin to imagine a solution in which we duplicate the template, swap in the correct values, that inject the new element into the DOM. And, that's exactly what we do.
First, let's set up the demo file:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Dynamic Upload Example</title>
<!-- Linked files. -->
<script type="text/javascript" src="jquery-1.2.6.min.js"></script>
<script type="text/javascript" src="dynamic_file_upload.js"></script>
</head>
<body>
<h1>
Dynamic Upload Example
</h1>
<form>
<div id="files">
<div id="file1" class="row">
1: <input type="file" name="presoFile1">
</div>
</div>
<p>
<a id="add-file-upload">Add File Upload</a>
</p>
<!--- DOM templates. --->
<div id="element-templates" style="display: none ;">
<div id="::FIELD1::" class="row">
::FIELD2::: <input type="file" name="::FIELD3::">
</div>
</div>
</form>
</body>
</html>
Here, you can see that we have your form with file list, a link to add new elements, and our hidden DIV with the element templates. Now, let's take a look at the Javascript file that is powering this demo (which was tested in FireFox 3, Internet Explorer 7 and 6):
// When the DOM has loaded, init the form link.
$(
function(){
// Get the add new upload link.
var jAddNewUpload = $( "#add-file-upload" );
// Hook up the click event.
jAddNewUpload
.attr( "href", "javascript:void( 0 )" )
.click(
function( objEvent ){
AddNewUpload();
// Prevent the default action.
objEvent.preventDefault();
return( false );
}
)
;
}
)
// This adds a new file upload to the form.
function AddNewUpload(){
// Get a reference to the upload container.
var jFilesContainer = $( "#files" );
// Get the file upload template.
var jUploadTemplate = $( "#element-templates div.row" );
// Duplicate the upload template. This will give us a copy
// of the templated element, not attached to any DOM.
var jUpload = jUploadTemplate.clone();
// At this point, we have an exact copy. This gives us two
// problems; on one hand, the values are not correct. On
// the other hand, some browsers cannot dynamically rename
// form inputs. To get around the FORM input name issue, we
// have to strip out the inner HTML and dynamically generate
// it with the new values.
var strNewHTML = jUpload.html();
// Now, we have the HTML as a string. Let's replace the
// template values with the correct ones. To do this, we need
// to see how many upload elements we have so far.
var intNewFileCount = (jFilesContainer.find( "div.row" ).length + 1);
// Set the proper ID.
jUpload.attr( "id", ("file" + intNewFileCount) );
// Replace the values.
strNewHTML = strNewHTML
.replace(
new RegExp( "::FIELD2::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD3::", "i" ),
("presoFile" + intNewFileCount)
)
;
// Now that we have the new HTML, we can replace the
// HTML of our new upload element.
jUpload.html( strNewHTML );
// At this point, we have a totally intialized file upload
// node. Let's attach it to the DOM.
jFilesContainer.append( jUpload );
}
In the AddNewUpload() method, we are doing exactly what we discussed above; duplicating the template node and inserting it into the DOM with updated values. The one tricky thing that we have to think about is that not all browsers can dynamically rename form fields (at least from what I can remember - maybe these days, all modern browsers can - it's been a while since I actually tested that feature, correct me if I'm wrong). To get around this, we actually flatten the inner HTML of the template, replace the values, then deserialize it back into a set of real DOM elements.
The jQueryElement.html() method acts as both a getter and setter. If it is called with no arguments, it returns the serialized HTML of that element. If it is invoked with an argument, it takes that argument (which should be HTML code), deserializes it, and makes it the child of the element.
NOTE: You could theoretically use the .html() method to build the target file upload DIV without the DOM template; however, as I stated above, the more complicated the target element gets, the bigger a pain in the butt this becomes.
Hope that helps. If nothing else, I hope that it has inspired you to look into jQuery; it's really the Javascript library to end all Javascript libraries.
Want to use code from this post? Check out the license.
Reader Comments
Here's a Prototype solution to the same problem, using Prototype's Template, Element.insert(), and a possibly gratuitous event-handler registration. When dynamically adding content to the DOM, I prefer registering event-handlers to using onClick="".
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Dynamic Upload Example</title>
<script type="text/javascript" src="prototype.js"></script>
</head>
<body>
<h1>
Dynamic Upload Example
</h1>
<form>
<div id="files">
<div id="file1" class="row">
1: <input type="file" name="presoFile1">
</div>
</div>
<p>
< a id="add-file-upload" href="##">Add File Upload< /a>
</p>
</form>
< script type="text/javascript" >
var iterator = 1;
var uplTemplate = new Template('<div id="element-templates"><div id="#{id}" class="row">#{label}: <input type="file" name="#{name}"></div></div>');
function insertNewTemplate(){
iterator++;
Element.insert($('files'),uplTemplate.evaluate({'id':'file' + iterator, 'name':'presoFile' + iterator, 'label':iterator}));
}
$('add-file-upload').observe('click',insertNewTemplate);
< /script >
</body>
</html>
@Matt,
I really like the way that Prototype abstracts out the template process. I think that would be a cool plugin for jQuery.
However, my thought is the same that I have with the jQuery .html() method. And that is, the larger the target element gets, the harder that is going to be to create, read, and maintain. I am not saying to never use it - for small things, its the cat's pajamas. But, for anything of size, I have really enjoyed the template method using existing DOM elements as it makes things very easy to maintain.
In the past, I have also gotten the template element via AJAX so that it doesn't have to exist in the same file. However, that requires another call to the server, so I use that when its part of a greater piece of functionality (where the Server actually generates the DOM based on passed-in parameters).
Ben,
Just wanted to share the following links to JavaScript templating solutions with you:
http://code.google.com/p/trimpath/wiki/JavaScriptTemplates
http://www.matthias-georgi.de/2008/9/patroon-a-javascript-template-engine.html
I've also found templating to be the best way to deal with adding to the DOM. I haven't used any of the solutions above yet, although I'll probably check out TrimPath first as I'm familiar with veloclity.
It's nice to see these libraries popping up so that you can easily do things like looping and not just simple string replacement which is what I normally resorted to.
Ben, you're my hero. Or you will be, if you can help me with this: I'm using your templating technique to spawn new form fieldsets, and your method is awesome. (Although I think it would be simpler to do one global ("g") replace in the regex, if you're just appending the same id in multiple places.) Now, say I want to have a generated select box display different options depending on the choice, i.e.,
select name="pet" class="pet-choice-1"
--cats
--lizards
if you select cats, a few new input boxes are unhidden with show(), if lizards, different boxes. In a static form I could do
$(".pet-choice-1).change(function() {
//do stuff
}
(and loop through the different pet-choice-x selects). But this doesn't seem to work since the new fieldsets (after #1) don't exist yet. Am I missing something?
Okay, I realise this is an internet hail mary, but as I've written this comment, I've come to feel that you're like a son to me, and indeed, I do have a son named Ben. Weird.
@AGD,
I understand what you are trying to do, but I am not sure that I follow the error. If you want, you can email me the page and I can take a look: ben at bennadel dot com.
@Kurt,
That's cool stuff. I never thought of using a hidden Textarea element to hold the template code. That's a pretty cool idea because it won't mess to much with the existing DOM... and the code is basically serialized already.
Ben, Thanks for the kind offer. I think I've finally figured out my issue, but again, your example code was really helpful.
@AGD,
Awesome. Glad you got it figured out.
@Kurt,
I've been playing around with the Textarea templates. Awesome stuff! Thanks for passing that along.
@Kurt,
Thanks a lot for showing me the Textarea idea. I turned it into a jQuery plug-in and its working like a charm. You the man.
www.bennadel.com/index.cfm?dax=blog:1393.view
I am trying to create the same as the example above, but I noticed the script files attached above. I have the JQUERY.JS and the prototype-1.6.0.3.js from http://www.prototypejs.org, but I can't seem to get the element to appear on the page for another file upload. Where can I get the script files you used for these examples? I have a feeling that is what is missing from the big picture.
Also, what kind of file is the DOM being saved as? I saved it as a .cfm file, not sure if that is okay or not. Though maybe it should be saved as a simple HTML file. Any suggestions would be greatly appreciated.
That is awesome, thanks! I just missed a "remove" link besides each input. :)
Ben I am very new to jquery and this was exactly what I was looking for. I'm running into something weird and I can't figure out what's causing it. I've got it setup and working, but when it clones the template div it's including the template div as well, so all of the new fields are hidden. This also means that for each new item I have an extra div on the outside. Any ideas at all? Thanks.
If only I had waited 30 more seconds before hitting the post button. Found my problem. Thanks for the example.
@M,
No problem - glad you figured it out.
Hey,
Thanks for the article! It has helped me to add some really funky functionality to my form.
Matt
Hello Ben,
This is a very neat idea you had here... but what if I wanted to make my file fields linked as in a PHP array? I've been puzzling over this one and even though it must be obvious to a pro I can't seem to pull it off... usually I'd just make all of the names photo[] but I've tried and it messes things up... Any ideas?
Thanks!
Andrea
@Andie,
I am not sure how PHP handles form posts. Generally, I try to avoid having any fields the same name except for radio and checkboxes. I am not sure what you want to do with that array?
Hello Ben,
Thanks for the reply, I actually figured it out a few days ago. This is my version of the upload field:
.replace(
new RegExp( "::FIELD3::", "i" ),
("colors[]")
)
and on the front end I have something like:
<label>1: </label> <input type="file" name="colors[]">
Works like a charm :) I just process the results as I would a normal array in PHP.
Thanks again for the great script!
Andrea
@Andie,
Sounds interesting. I wish I knew more about PHP.
I wish I knew half of what you do know already Hehehe!!!! :)
@Andie,
Ha ha, thanks :)
Hi @Ben
on Linux (used FF and Chrome) the script doesn't work as it should.
Look this screenshot (http://i99.photobucket.com/albums/l301/jasvazquez/Pantallazo.png)
Thanks for your work!
@Informático,
That's very strange! It's like the length value is not being calculated properly.
That is a good example is there way to do dynamic insert of template in between to inserts
@Erik,
I am sorry, I don't understand what you're asking.
Hi Ben, your script is great. I want to try to make a Remove buttom, but I'm lost here! Can you give me any idea to make this?
@Allan,
You would just want to add a "Remove" or "Delete" link to the template, then bind a click event handler to it that removes the containing Div. So, since the container div has the class "row", we could do something like this:
You'd have to either bind this event handler after each new Row is added. Or, you could bind this to the container of the rows, and then handle it via event delegation.
Hi guys. AGD may have been asking a similar thing, but here's what I wanted to do. I am relatively new to JQuery as I have done some but not a lot of development using it. Anyway, what I am wanting to do is access one of the new form fields I created using JQuery and change onKeyUp. What I am essentially trying to do is onKeyUp, access the value of one of the dynamically created form fields, do a function on it with the value of another one of the dynamically created form fields, and set the value of a third dynamically created form field to the result. I apologize if this sounds convoluted. I am working with a site that is currently working as a static site and is using jQuery to perform these functions, but we are wanting to grow the fields dynamically, making the application more scalable. Any insights? Thanks so much in advance, and I apologize ahead of time for any ignorance, as again, I am relatively new to JQuery. Thanks!
@Allie,
Once you get the cloned HTML markup (the stuff that we are adding to the page), you can use the .find() method to locate the Input field within the markup.
jUpload.find( "input" ).bind( "keyup", function(){ ... });
Once you have the embedded input field, you can then bind the keyup event to it.
Thanks so much, Ben! I got that part working. Now if only I can get my ColdFusion on the other side (after submission) to recognize these new fields! :-/ I'm gonna read all of the other comments here to your post to see if there is any insight in there. Thanks a lot!!!!
hi, iam using your code, that was very awesome sorry if my english is bad, i want to ask how to limit the field maybe just can add total 10 filed. Thanx
This is useful, here is a quick way I used to do the same (no templating):
function AddNewUpload(){
var jFilesContainer = $( "#files" );
var intNewFileCount = (jFilesContainer.find( "div.row" ).length + 1);
if(intNewFileCount > 5) return false;
var jUpload = '<div data-role="fieldcontain" class="row">'+
'<label for="upload_file'+intNewFileCount+'">Image file:</label>'+
intNewFileCount+': <input type="file" name="upload_file'+intNewFileCount+'" id="upload_file'+intNewFileCount+'" value="" />'+
'</div>';
jFilesContainer.append( jUpload );
} // end function
@Allie, @Ace,
Glad you guys liked it - happy to help.
@AjaxLover,
No doubt, there's a time and place for "templating" and a time and place for just getting it done. I don't think either approach is any more intrinsically good - whichever solution works best for you at the time.