Twitter: raymondcamden


Address: Lafayette, LA, USA

Multi-File Uploads and Multiple Selects (Part 2)

04-14-2014 4,701 views JavaScript, HTML5 29 Comments

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.

Related Blog Entries

29 Comments

  • Commented on 04-14-2014 at 12:44 PM
    I have played around several times with file uploads but have never taken the jQuery method for uploading files. I have seemed to have success with just CF. I may have to create a demo with this example you created and do some testing with it for possible future coding options. Cool, hope you had a good weekend.
  • Chris Bowyer #
    Commented on 04-14-2014 at 7:41 PM
    Slightly off topic, but I had a good play with Adobe FormsCentral last night and quite frankly I am blown back. Also got me thinking, who needs sever validation if you need JavaScript to see the form.
  • Commented on 04-14-2014 at 8:08 PM
    You always need server-side validation. Period. You need not have "friendly server-side validation, i.e. you may just output ERROR, but you must* check your input on the server. Period.

    Let's take your example of a form that isn't even visible unless you have JS. I'd simply run the page, look at where it POSTS, and I'd try to attack your server that way.
  • Chris Bowyer #
    Commented on 04-14-2014 at 8:22 PM
    D'oh! Didn't even think about that. Suppose the same if it's just a JavaScript link to the form too.
  • karim #
    Commented on 05-16-2014 at 1:49 PM
    Détails de l’erreur de la page Web

    Agent utilisateur : Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; BTRS98585; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
    Horodateur : Fri, 16 May 2014 18:43:15 UTC

    Bonjour,
    Félicitation pour ce programme..
    Je l'ai essayer pour me faire un site ou je gère une gallerie de peintures
    ce programme de download est super par contre il fonctionne sous Firefox mais sous IE8 j'ai l'erreur ci-dessous :

    Message : Objet attendu
    Ligne : 32
    Caractère : 3
    Code : 0

    y à t'il une solution pour que ça marche ?
  • Commented on 05-16-2014 at 2:16 PM
    As I don't speak French (and I'm not sure why you would think I do as I've never used it on the blog), I can't help you.
  • Commented on 05-16-2014 at 3:18 PM
    Karim, try moving the document ready code to the end of that script block so the two function definitions are before it.
  • Commented on 05-16-2014 at 4:25 PM
    Hmm, actually, thinking about that the functions should be hoisted above the document ready stuff... right Ray?

    I counted line 32 to be the document ready and just assumed the missing object error was due to that but I haven't actually tested this on IE8 (which I have on a VM)... so I should probably do that before offering any assistance :)
  • karim #
    Commented on 05-16-2014 at 5:27 PM
    Hello I'm happy that you are to give me a rapid reponce.
    i don't speak English ...
    excuse me if i do some error when i script.
    a'm added some lines in your code and now there is not error in the new script.
    But there is away no image on the screen.
    The Code I'm added provide to different sites and examples for to compensate the compatibility on IE8.
    I'm going to see the address of SCR perhaps I'm while find Where is my error.

    Merci pour votre aide.
    Cordialement.


    <!doctype html>
    <html>
    <head>
    <title>Proper Title</title>
    <style>
    /   #selectedFiles img {
          max-width: 200px;
          max-height: 200px;
          float: left;
          margin-bottom:10px;
       }
    /

    #selectedFiles img {
    width: 160px;
    height: 120px;
    border:1px solid;
    float:right;
    filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale);
    }
    </style>

    </head>

    <body>
       <div id="selectedFiles"></div>
       <form id="myForm" method="post">

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



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

    <!--<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.4/...;
    <script type="text/javascript" src="jquery-git1.js"></script>
       <script language="javascript">
       var selDiv = "";
       var storedFiles = [];
       //$(document).ready(function() {
          jQuery(document).ready(function(){
          $("#files").on("change", handleFileSelect);
          selDiv = $("#selectedFiles");
          $("#myForm").on("submit", handleForm);
          
          $("body").on("click", ".selFile", removeFile);
       });
       
    /
    Shim for "fixing" IE's lack of support (IE < 9) for applying slice
    on host objects like NamedNodeMap, NodeList, and HTMLCollection
    (technically, since host objects have been implementation-dependent,
    at least before ES6, IE hasn't needed to work this way).
    Also works on strings, fixes IE < 9 to allow an explicit undefined
    for the 2nd argument (as in Firefox), and prevents errors when
    called on other DOM objects.
    /
    (function () {
    'use strict';
    var slice = Array.prototype.slice;

    try {
    // Can't be used with DOM elements in IE < 9
    slice.call(document.documentElement);
    }
    catch (e) { // Fails in IE < 9
    Array.prototype.slice = function (begin, end) {
    var i, arrl = this.length, a = [];
    // Although IE < 9 does not fail when applying Array.prototype.slice
    // to strings, here we do have to duck-type to avoid failing
    // with IE < 9's lack of support for string indexes
    if (this.charAt) {
    for (i = 0; i < arrl; i++) {
    a.push(this.charAt(i));
    }
    }
    // This will work for genuine arrays, array-like objects,
    // NamedNodeMap (attributes, entities, notations),
    // NodeList (e.g., getElementsByTagName), HTMLCollection (e.g., childNodes),
    // and will not fail on other DOM objects (as do DOM elements in IE < 9)
    else {
    // IE < 9 (at least IE < 9 mode in IE 10) does not work with
    // node.attributes (NamedNodeMap) without a dynamically checked length here
    for (i = 0; i < this.length; i++) {
    a.push(this[i]);
    }
    }
    // IE < 9 gives errors here if end is allowed as undefined
    // (as opposed to just missing) so we default ourselves
    return _slice.call(a, begin, end || a.length);
    };
    }
    }());


    'use strict';

    // Add ECMA262-5 method binding if not supported natively
    //
    if (!('bind' in Function.prototype)) {
    Function.prototype.bind= function(owner) {
    var that= this;
    if (arguments.length<=1) {
    return function() {
    return that.apply(owner, arguments);
    };
    } else {
    var args= Array.prototype.slice.call(arguments, 1);
    return function() {
    return that.apply(owner, arguments.length===0? args : args.concat(Array.prototype.slice.call(arguments)));
    };
    }
    };
    }

    // Add ECMA262-5 string trim if not supported natively
    //
    if (!('trim' in String.prototype)) {
    String.prototype.trim= function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
    };
    }

    // Add ECMA262-5 Array methods if not supported natively
    //
    if (!('indexOf' in Array.prototype)) {
    Array.prototype.indexOf= function(find, i /opt/) {
    if (i===undefined) i= 0;
    if (i<0) i+= this.length;
    if (i<0) i= 0;
    for (var n= this.length; i<n; i++)
    if (i in this && this[i]===find)
    return i;
    return -1;
    };
    }
    if (!('lastIndexOf' in Array.prototype)) {
    Array.prototype.lastIndexOf= function(find, i /opt/) {
    if (i===undefined) i= this.length-1;
    if (i<0) i+= this.length;
    if (i>this.length-1) i= this.length-1;
    for (i++; i-->0;) / i++ because from-argument is sadly inclusive /
    if (i in this && this[i]===find)
    return i;
    return -1;
    };
    }
    if (!('forEach' in Array.prototype)) {
    Array.prototype.forEach= function(action, that /opt/) {
    for (var i= 0, n= this.length; i<n; i++)
    if (i in this)
    action.call(that, this[i], i, this);
    };
    }
    if (!('map' in Array.prototype)) {
    Array.prototype.map= function(mapper, that /opt/) {
    var other= new Array(this.length);
    for (var i= 0, n= this.length; i<n; i++)
    if (i in this)
    other[i]= mapper.call(that, this[i], i, this);
    return other;
    };
    }
    if (!('filter' in Array.prototype)) {
    Array.prototype.filter= function(filter, that /opt/) {
    var other= [], v;
    for (var i=0, n= this.length; i<n; i++)
    if (i in this && filter.call(that, v= this[i], i, this))
    other.push(v);
    return other;
    };
    }
    if (!('every' in Array.prototype)) {
    Array.prototype.every= function(tester, that /opt/) {
    for (var i= 0, n= this.length; i<n; i++)
    if (i in this && !tester.call(that, this[i], i, this))
    return false;
    return true;
    };
    }
    if (!('some' in Array.prototype)) {
    Array.prototype.some= function(tester, that /opt/) {
    for (var i= 0, n= this.length; i<n; i++)
    if (i in this && tester.call(that, this[i], i, this))
    return true;
    return 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 = "<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>
  • Commented on 05-16-2014 at 6:42 PM
    If you can, please do not post large blocks of code here. My auto formatting tends to break some of it. In the future, use a Gist or Pastebin instead. Is this online where I can see it and run it?
  • karim #
    Commented on 05-17-2014 at 2:14 AM
    OK ,

    I' continuous to work on this code.. and I'm give you regularity my results about it ..
  • mickey #
    Commented on 06-04-2014 at 10:51 AM
    Hello, Raymond. Thank you for the code. I have tried to put them into my page. But I found that even the file has been removed from the list displaying in the page, the file still uploaded (or say Post) to the server. Is the reason (removed file) still contain in the input tag?
  • mickey #
    Commented on 06-04-2014 at 12:09 PM
    I now know that the handleForm is triggered by <input type="submit"> , right ? But my form is using a button to first go to a javascript to valid the form before submission. document.getElementById("myform").submit(); I will use it to subit the form. So how can i change the method to prevent uploading the file which is removed ?
  • Commented on 06-04-2014 at 12:52 PM
    Without seeing your code I really can't comment. You would need to completely stop the form submit and chain off to my code after validation. If you are using other form fields then they would need to be appended too.
  • jack #
    Commented on 06-20-2014 at 5:05 AM
    Hi, i am testing your code, but it is uploading only one file from more.
    Any help?
  • Commented on 06-20-2014 at 8:55 AM
    Put it online where I can test and I'll try and see if something stands out in the console.
  • vinay #
    Commented on 07-05-2014 at 12:27 AM
    files not removed.

    var file = $(this).data("file");
    alert(file);

    alert shows undefined....why?
  • Commented on 07-05-2014 at 9:44 AM
    @vinay: I don't understand your comment. Can you add more?
  • Rigin #
    Commented on 07-22-2014 at 2:09 AM
    Am trying your code in MVC 4
    But , function removeFile(e) {.... this function is deleting from temp array - storedFiles,
    But while posting all the selected files are posted , Deletion NOT Working :(
  • Rigin #
    Commented on 07-22-2014 at 2:13 AM
    Pls check this :
    http://pastebin.com/ZFZux1ck
  • Commented on 07-22-2014 at 9:32 AM
    I just tested my code again in Chrome and it worked correctly. Your code does not match mine, so I'd recommend updating it.
  • Rigin #
    Commented on 07-23-2014 at 3:06 AM
    Hi,
    i tried , Am using asp MVC :
    from my code : xhr.send(data);
    is not getting fired to my controller action.
    What could be possible reason ?
  • Commented on 07-23-2014 at 8:00 AM
    Open up the developer console on your browser and see if an error is being reported. You can also see if the submit handler is firing correctly.
  • Rigin #
    Commented on 07-23-2014 at 8:09 AM
    Hi Raymond,

    Your code is working fine.Error was at my end.
    Thanks for the same

    I have one more issue.

    I need to post the form data also.I am using mvc4.
    If i comment the e.preventdefault() the form data gets posted, but the attachments that i deleted is not removed.

    How can this be done.(Post the form data as well as upload the attachments with the remove functionality intact)

    Please refer the code snippet
    http://pastebin.com/Pi7NTyMP

    Regards,
    Rigin
  • Commented on 07-23-2014 at 8:21 AM
    "If i comment the e.preventdefault() the form data gets posted, but the attachments that i deleted is not removed."

    Ok, so then the remove action is not working for you. Think about it then - how would you debug this? I'd add console.log messages to the remove action to ensure that the delete is working properly. You can check the length of the array for example to ensure it is one less.
  • Rigin #
    Commented on 07-23-2014 at 8:46 AM
    After removing one action, the posted all files are getting uploaded ... removed one also.
    Then how can we get the updated list of attachments in action ? can you brief please
  • Commented on 07-23-2014 at 9:01 AM
    I do not know what you mean - "by removing one action".
  • Rigin #
    Commented on 07-23-2014 at 10:14 AM
    See what I meant is : am getting all the files that we selected in the input file element in my action method .
    But the model am that received in action is empty one.

    How can I get the model from form where am firing this submit ? I also need the other form data..textbox, dropdown and all...
    are you clear with my query ? please let me know
  • Commented on 07-23-2014 at 10:46 AM
    "But the model am that received in action is empty one."
    I do not understand what you mean here. If you use your Dev Tools and see all the files being posted (which you can do with Chrome's Network tools), then the issue is with your server-side code and I can't help you with that.

    "How can I get the model from form where am firing this submit ? I also need the other form data..textbox, dropdown and all..."
    In my example, I create a form post consisting of JUST the files. You can add additional file fields too. See the docs for FormData: https://developer.mozilla.org/en-US/docs/Web/API/F...

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