Multi-File Uploads and Multiple Selects (Part 2)

A while back I wrote a simple example of using JavaScript to add file previews for a multi-file upload HTML control. You can find that entry here: Adding a file display list to a multi-file upload HTML control. I followed it up with another example (Multi-File Uploads and Multiple Selects) that demonstrated adding support for multiple selections. This weekend a reader asked for a way to remove files from the list before uploading. Here is an example of that.

First – I had to figure out how users would remove files. I could have added a button to each image preview, or a link. Anything really. But to make things simpler, I decided that a click on the image would remove it. Obviously that may not be the best UX. I added a title attribute to help make this clear. You should be able to easily modify my code to change how this works. Let’s look at the code and then I’ll explain the changed bits. (If you didn’t read the previous entries though, please do so. I won’t be going over the basics again.)

<!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 type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
	
	<script>
	var selDiv = "";
	var storedFiles = [];
	
	$(document).ready(function() {
		$("#files").on("change", handleFileSelect);
		
		selDiv = $("#selectedFiles"); 
		$("#myForm").on("submit", handleForm);
		
		$("body").on("click", ".selFile", removeFile);
	});
		
	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 = "<div><img src=\"" + e.target.result + "\" data-file='"+f.name+"' class='selFile' title='Click to remove'>" + f.name + "<br clear=\"left\"/></div>";
				selDiv.append(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);
	}
		
	function removeFile(e) {
		var file = $(this).data("file");
		for(var i=0;i<storedFiles.length;i++) {
			if(storedFiles[i].name === file) {
				storedFiles.splice(i,1);
				break;
			}
		}
		$(this).parent().remove();
	}
	</script>

</body>
</html>

The first big difference in this version is the use of jQuery. I didn’t really need it before so I used querySelector instead. I needed to make use of jQuery’s simple handling of post-DOM manipulation event binding (let me know if that doesn’t make sense) so I added in the library. I’ve added my click handler here:

$("body").on("click", ".selFile", removeFile);

I then modified the image display to include the class and title attribute.

var html = "<div><img src=\"" + e.target.result + "\" data-file='"+f.name+"' class='selFile' title='Click to remove'>" + f.name + "<br clear=\"left\"/></div>";

Notice I added a div around the image and file name. This will make sense in a second. Now let’s look at the handler.

function removeFile(e) {
	var file = $(this).data("file");
	for(var i=0;i<storedFiles.length;i++) {
		if(storedFiles[i].name === file) {
			storedFiles.splice(i,1);
			break;
		}
	}
	$(this).parent().remove();
}

Not really rocket science. I find the file in the existing list, remove it, and then remove the image/file text from the DOM. Done.

  • David Griffiths

    Hi Raymond, thank you for this very useful information. I have managed to get the handleForm’s output data into a controller where I manipulate the images.The problem is I can’t then send the images to a view with “View::make ->with ” . No error messages but Laravel refuses to do it. Do I need to use return Response::json([]) and can you advise me how I could do this.

    • http://www.raymondcamdencom/ Raymond Camden

      I’m sorry, but I don’t even know what Laravel is.

      • David Griffiths

        Sorry, it was presumptuous of me to assume that you would. Looking at your code in handleForm I can take the FormData object and have lots of fun with the images in a PHP code block but when I try to export the images to another page the fun stops. The same setup without jQuery works fine so I am assuming jQuery needs something returned (like with Response::json()). Just wondered if you could give me an idea how to implement that with your code in handleForm. Thanks.

        • http://www.raymondcamdencom/ Raymond Camden

          Um… so to be clear. My code works to *post* to your server, but you don’t know how to respond back to it to let it know that… it did something? Not quite sure I get what you mean. Also, I don’t really know PHP. If your server side code needs to return a message to the client side code, you should probably reply with a JSON encoded msg. How that is done in PHP is not something I can help with.