Multi-File Uploads and Multiple Selects

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.