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.

  • Desperant

    Hi, how can i read storedFiles array with files in PHP? Thanks a lot

    • http://www.raymondcamdencom/ Raymond Camden

      You can’t – it is a client-side variable. You can use XHR to send JS data to the server of course.

  • Dipok Chakraborty

    I tried to apply this technique in my module but after remove the selected file and submit the form then all of files are uploaded including removed files. i think my form is not update after remove file. how can i fix it?

    • http://www.raymondcamdencom/ Raymond Camden

      Is it online where I can test it? Do you see an error in the console?

      • Dipok Chakraborty

        Sorry for my late. no its on my local server. if you want i can send you by mail.

        • http://www.raymondcamdencom/ Raymond Camden

          Typically code review of this sort is a paid engagement only. If you put it online though I don’t mind giving it a quick look. I’d suggest looking in the console to see if you see an error.

    • Dipok Chakraborty

      How can i get files content after delete from #files

      • http://www.raymondcamdencom/ Raymond Camden

        Not sure what you mean in this comment. To the first comment, I’d look for an error in the console.

        • Dipok Chakraborty

          before append data, there is any way to know how many files in my input field

          • http://www.raymondcamdencom/ Raymond Camden

            Yes – it is an array and you can check the length.

          • Dipok Chakraborty

            how?

          • http://www.raymondcamdencom/ Raymond Camden

            Given X is an array, in JS you can just do X.length.