Twitter: raymondcamden


Address: Lafayette, LA, USA

Multi-File Uploads and Multiple Selects

10-01-2013 16,251 views Development, HTML5 9 Comments

Edit on 12/6/2013: I forgot to update this post based on a bug from my earlier post. I used a for loop when forEach should have been used. I corrected this in the final code sample.

A few weeks back I wrote a blog post about adding image previews for multi-file upload controls. I didn't mention it at the time but I had an ulterior motive. A reader wrote to me a few weeks before with an interesting question.

Is it possible to use a mult-file input control and let the user select multiple times?

To be clear, what we mean here is that the user selects some files and closes the file picker dialog. She then realizes she forgot a few files and clicks to select them next.

What happens in this situation is pretty simple. Like the multiple select field, if you pick something else then the previous selection is removed. Your only option is similar to what you do for the drop down. Use ctrl/cmd to select multiple files in multiple folders all at once - and don't screw it up! Obviously most users won't be able to grok this and will screw it up, even if they know it is possible.

But my experiment had given me an idea. Remember that we can use an event handler to detect changes to the input field and get access to the file data beneath. Here is a code snippet showing this:

function handleFileSelect(e) {
		
	if(!e.target.files) return;
		
	selDiv.innerHTML = "";
		
	var files = e.target.files;

	for(var i=0; i";

	}
		
}

Based on this, my final demo uses this code to create image thumbnails based on pictures you select. My demo has a bug though that meshes well with today's blog post. If you select images twice, the list of thumbnails grow, but the actual files associated with the control are only based on the last selection. But what if we could take those files and store them?

Before I went down this route, I updated my demo code to use AJAX to post the form. Part of the benefits of XHR2 is the ability to send file data over the wire. Let's look at a simple example of this.

<!doctype html>
<html>
<head>
<title>Proper Title</title>
<style>
	#selectedFiles img {
		max-width: 200px;
		max-height: 200px;
		float: left;
		margin-bottom:10px;
	}
</style>
</head>
    
<body>
	
	<form id="myForm" method="post">

        Files: <input type="file" id="files" name="files" multiple><br/>

        <div id="selectedFiles"></div>

        <input type="submit">
	</form>

	<script>
	var selDiv = "";
		
	document.addEventListener("DOMContentLoaded", init, false);
	
	function init() {
		document.querySelector('#files').addEventListener('change', handleFileSelect, false);
		selDiv = document.querySelector("#selectedFiles");
		document.querySelector('#myForm').addEventListener('submit', handleForm, false);
	}
		
	function handleFileSelect(e) {
		var files = e.target.files;
		for(var i=0; i<files.length; i++) {
			var f = files[i];
			if(!f.type.match("image.*")) {
				continue;
			}

			var reader = new FileReader();
			reader.onload = function (e) {
				var html = "<img src=\"" + e.target.result + "\">" + f.name + "<br clear=\"left\"/>";
				selDiv.innerHTML += html;
				
			}
			reader.readAsDataURL(f); 
		}
		
	}
		
	function handleForm(e) {
		e.preventDefault();
		console.log('handleForm');
		var data = new FormData(document.querySelector('#myForm'));
				
		var xhr = new XMLHttpRequest();
		xhr.open('POST', 'handler.cfm', true);
		
		xhr.onload = function(e) {
			if(this.status == 200) {
				console.log('onload called');
				console.log(e.currentTarget.responseText);
				
			}
		}
		
		xhr.send(data);
	}
	</script>

</body>
</html>

If we focus on the changes, the only real difference is that we have a submit handler for the form. We use a FormData object to package up our form and then post it to a server-side handler. The server-side code isn't terribly important. It doesn't see this as anything "special" or "Ajax-y" (my word), it is just a form post. But now the entire process runs through Ajax and not a traditional page reload. (And as a note, I'm not providing any user feedback here. In a real application I'd disable the submit button, tell the user something, etc etc.)

That parts done, now let's try storing a copy of the files. Here is my updated version with this in action.

<!doctype html>
<html>
<head>
<title>Proper Title</title>
<style>
	#selectedFiles img {
		max-width: 200px;
		max-height: 200px;
		float: left;
		margin-bottom:10px;
	}
</style>
</head>
    
<body>
	
	<form id="myForm" method="post">

        Files: <input type="file" id="files" name="files" multiple><br/>

        <div id="selectedFiles"></div>

        <input type="submit">
	</form>

	<script>
	var selDiv = "";
	var storedFiles = [];
		
	document.addEventListener("DOMContentLoaded", init, false);
	
	function init() {
		document.querySelector('#files').addEventListener('change', handleFileSelect, false);
		selDiv = document.querySelector("#selectedFiles");
		document.querySelector('#myForm').addEventListener('submit', handleForm, false);
	}
		
	function handleFileSelect(e) {
		var files = e.target.files;
		var filesArr = Array.prototype.slice.call(files);
		filesArr.forEach(function(f) {			

			if(!f.type.match("image.*")) {
				return;
			}
			storedFiles.push(f);
			
			var reader = new FileReader();
			reader.onload = function (e) {
				var html = "<img src=\"" + e.target.result + "\">" + f.name + "<br clear=\"left\"/>";
				selDiv.innerHTML += html;
				
			}
			reader.readAsDataURL(f); 
		});
		
	}
		
	function handleForm(e) {
		e.preventDefault();
		var data = new FormData();
		
		for(var i=0, len=storedFiles.length; i<len; i++) {
			data.append('files', storedFiles[i]);	
		}
		
		var xhr = new XMLHttpRequest();
		xhr.open('POST', 'handler.cfm', true);
		
		xhr.onload = function(e) {
			if(this.status == 200) {
				console.log(e.currentTarget.responseText);	
				alert(e.currentTarget.responseText + ' items uploaded.');
			}
		}
		
		xhr.send(data);
	}
	</script>

</body>
</html>

The changes are pretty simple. I've got a new global variable called storedFiles. When I detect a change on the input field, I now push them into this array. Finally, when the form is submitted, instead of pre-populating the FormData object we create it empty and then simply append our files. Note the append call uses the same name, files, so that when the server processes it the name is consistent.

And... believe it or not - this worked. This smells like it may be a slight security concern. I have to imagine that if browser vendors allow for this then it must be safe, but if I used this in production, I'd be real sure to let the end user know what is going on. As I said my previous demo actually implied it was doing this anyway. (I should have been clearing out my thumbnails when you selected files.) I think in that case the user would have expected it.

Related Blog Entries

9 Comments

  • Commented on 10-22-2013 at 10:22 AM
    Great post! One question, minor point? If the user chooses to select files more than once, and selected a file they'd already chosen (those silly users), would it show up more than once in the storedFiles collection? And would this actually be a problem?
  • Commented on 10-22-2013 at 10:35 AM
    I believe it would. So the fix would be to check the array before adding to it.
  • Commented on 10-22-2013 at 10:21 PM
    I was just poking at one of the very popular jQuery uploaders, and it also allows duplicates if you add more files.
  • Mikelarg Reu #
    Commented on 11-12-2013 at 9:20 AM
    Thank you that post help me finish one thing!
  • Mathew #
    Commented on 12-06-2013 at 9:49 AM
    With the code above the thumbnails are shown but they are all named as the last file I select. Is there a problem in this code or am I missing something else?
  • Commented on 12-06-2013 at 9:52 AM
    See the linked blog entry. I made a mistake that was corrected in the earlier one but not this one. Give me a few and I'll edit this blog post too.
  • Commented on 12-06-2013 at 10:15 AM
    Corrected here. Thank you for finding this.
  • sabarinath #
    Commented on 03-24-2014 at 1:48 AM
    can we able to call the all images to server side.if possible plz give me some suggestion.I am going to implement in my project
  • Commented on 03-24-2014 at 5:30 AM
    @sabarinath: I'm sorry, but I have no idea what you are saying.

Post Reply

Please refrain from posting large blocks of code as a comment. Use Pastebin or Gists instead. Text wrapped in asterisks (*) will be bold and text wrapped in underscores (_) will be italicized.

Leave this field empty