Drag and Drop and Automatically Send to the Server

by .

I realised (when looking myself) that there are a lot of demos and tutorials that show you how to drag-and-drop a file into the browser and then render it on the page. They're often labelled as "drag-and-drop and upload", but they actually don't upload. This tutorial will take you that final step.

I'll walk you through an example that you can play with: drag a file into the web page, and it'll be uploaded to the server instantly.

Tasks

Let's break this process down into specific tasks:

  1. Capture the drop event and read its data.
  2. Post that binary data to the server.
  3. Provide feedback on the progress of the upload.
  4. Optionally render a preview of what's being uploaded and its status.

To achieve all of this, we need the following HTML5 and non-HTML5 APIs:

  1. Drag and Drop
  2. FormData
  3. XHR progress event
  4. FileReader

Keep in mind that not all browsers support all of this technology today, but they're getting close. I just wanted to create a clear and complete tutorial on how to go the whole hog and upload that dropped file.

The end result: we're able to drop the file anywhere in the browser, we get a preview and progress of the upload, and it's a slick experience.

Drag and drop upload
Our drag-and-drop example page

Drag and Drop

Just as a forewarning, drag-and-drop has been known to be a bit of a killjoy. In fact, it's a nightmare, but I won't repeat what's been said before.

Our example is going to allow you to drag-and-drop a file anywhere within the browser. To achieve that, we need to attach our event listeners to the body or documentElement (i.e., the root HTML node). By listening on the documentElement, this code could be anywhere in the page, as documentElement will always exist. You can only listen on body once the <body> element has been encountered.

Here's the pattern of code we need to capture the drop event on the entire document:

var doc = document.documentElement;
doc.ondragover = function () { this.className = 'hover'; return false; };
doc.ondragend = function () { this.className = ''; return false; };
doc.ondrop = function (event) {
  event.preventDefault && event.preventDefault();
  this.className = '';

  // now do something with:
  var files = event.dataTransfer.files;

  return false;
};
Basic pattern for capturing drop events

I'm using the hover class so that I can toggle a tip explaining to the user that the file can be dropped on the page.

Inside the drop event, we can access the dropped files via event.dataTransfer.files.

An Alternative to Drag and Drop

Annoyingly, as I write this, I realise this combination of API requirements is currently only met by Chrome and Firefox. So, taking the test from Modernizr, we can test for support and provide our alternative:

var dndSupported = function () {
  var div = document.createElement('div');
  return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
};

if (!dndSupported()) {
  // take alternative route
}
Testing for drag-and-drop support

Instead of drag-and-drop, we can insert a file input element (I've given it an id of "upload"), and when its value is changed, the file can be scooped in:

document.getElementById('upload').onchange = function (event) {
  // `this` refers to the element the event fired upon
  var files = this.files;
};
An alternative to native drag-and-drop

You can see the equivalent is simply the files property of the HTMLInputElement object. This will give us access to the same file as the event.dataTransfer.files from the drop event.

Automatically Uploading the File

This is the neat bit, and it's painfully simple. We use a feature of the XHR2 spec: FormData. Once we create an instance of FormData, we can append items to it:

var formData = new FormData();
for (var i = 0; i < files.length; i++) {
  formData.append('file', files[i]);
}

// now post a new XHR request
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function () {
  if (xhr.status === 200) {
    console.log('all done: ' + xhr.status);
  } else {
    console.log('Something went terribly wrong...');
  }
};

xhr.send(formData);
Creating and posting FormData

Yeah, that's it.†

Let's look at a couple of key things that are going on the above code.

formData.append('file', files[i]);

We're sending named parameters to our server, specifically an array of values called file. Obviously, you can call it what you want, but the file is the name your server will be looking for when it saves the uploaded file (or files).

xhr.onload = function () {
  if (xhr.status === 200) {
    console.log('all done: ' + xhr.status);
  } else {
    console.log('Something went terribly wrong...');
  }
};

If you're familiar with XHR, you'll notice we aren't listening to onreadystatechange, only onload — which is (in effect) a convenience function to let you know when the readyState is 4 (i.e., loaded!). You should still check and respond appropriately to the status code of the request to ensure it's 200 OK, rather than a 500 (internal server error) or 404 (not found) or anything else.

xhr.send(formData);

The nice trick here is that XHR has automatically set the encoding of the posted data to multipart/form-data. This encoding allows your server to read and save the files. It's like the encoding used when you send an attachment in an email.

Providing Feedback to the User

XHR2 now (flippin' finally) comes with a progress event. So if you're sending or retrieving a large file via XHR, you can tell the user how far along you are.

It's pretty simple. If you want to track the progress of the XHR request, listen to the progress event. One gotcha that caught me out for some time: I needed to track the progress of the upload, not the download. To do this properly, you need to listen for progress on the XHR upload object, as so:

xhr.upload.onprogress = function (event) {
  if (event.lengthComputable) {
    var complete = (event.loaded / event.total * 100 | 0);
    progress.value = progress.innerHTML = complete;
  }
};

xhr.onload = function () {
  // just in case we get stuck around 99%
  progress.value = progress.innerHTML = 100;
};
Providing feedback with XHR2's progress event

Note that instead of xhr.onprogress, we use xhr.upload.onprogess. When this event fires, we check that the event supports calculating the amount of data uploaded (the lengthComputable part), and then calculate the amount completed.

In my HTML, I'm using a <progress> element (similar to the <meter> element, but more semantically appropriate as we're presenting the progress of a task) to show the user how much of their file has been uploaded:

<progress id="progress" min="0" max="100" value="0">0</progress>

Note that I'm using innerHTML for browsers that don't yet support <progress>. Then with a dash of CSS-generated content, both the innerHTML and value of the progress can be the same (perhaps an over optimisation, but I was rather proud of myself at the time!).

And Finally, Rendering a Preview

As a nice addition, we'll give the user a preview of what we're uploading for them. It requires the File API — specifically the FileReader API.

As the drag-and-drop operation (or the input[type=file]) contains a file object, we can hand this over to the FileReader to get the contents of the file. If it's something like an image, we can get the Base64 data and give the user a preview. Or if it's text, we could render it in a <div>. The MIME type for the file is available as the type property on the file object.

So let's assume we only accept images. Here's the preview part that runs after the file has been received by the browser:

var acceptedTypes = {
  'image/png': true,
  'image/jpeg': true,
  'image/gif': true
};

if (acceptedTypes[file.type] === true) {
  var reader = new FileReader();
  reader.onload = function (event) {
    var image = new Image();
    image.src = event.target.result;
    image.width = 100; // a fake resize
    document.body.appendChild(image);
  };

  reader.readAsDataURL(file);
}
Rendering a preview of an uploaded image

We create the FileReader and give it a file to read. In the above example, I've used readAsDataURL, but you can also read files in plain text and binary formats (as per the spec).

When the reader has read the file and the data is available, it fires the load event, which in turn creates our new image.

The result of the file read action is in the event.target.result property. In the case of the readAsDataURL, the value is along the lines of data:image/png;base64,ABC....

Using All the Tools in the Shed

This example may be a little contrived for the article that I wanted to write. I've used many different technologies to do something that's a fairly common pattern on the web. In fact, it was because I had trouble finding a definitive source on uploading to the actual server (except, of course, as I wrote this article I found this example from back in late 2010!).

Hopefully, you'll see how each bit of tech can work together to create an application, but equally I hope that you can see where this would work if none or just some of the technology wasn't available. Make sure you detect functionality. Make sure you provide fallbacks. Make sure you develop responsibly.

Here's the final drag-and-drop and upload demo for you to play with.

38 Responses on the article “Drag and Drop and Automatically Send to the Server”

Paco says

Hi Remy,

to render a preview, you can use as well:

window.URL.createObjectURL(file);

But you definitely should check for windows.URL and set it properly for some older browsers:

// hack for older chrome versions
window.URL = window.URL || window.webkitURL;

Cheers and thanks for the post,
Paco

Shiv Kumar says

Hey Remy,

Nice article on the combination of of drag-drop and file uploading using Html 5. Truly something that is used so often these days on websites.

As regards Html 5 File upload, I had written and article on that even before the article you point to.
Html5 File Upload with Progress.

You should probably also check the dropped file to make sure it is a type that can be pre-viewed.

@_dhar says

Hi,
I wrote a post on a similar topic (i.e. uploading files). It’s more focused on feature detection and possible fallbacks but I found it might be a good complement to your article.
File Upload Form: Feature Detection.
Cheers.

Caleb says

Thanks for the step by step. It’s helpful.

I’m still wondering where the file goes. I know we’re posting “that binary data to the server” but I don’t know where in the code we’ve specified a path where the file is supposed to be stored.

Russell Bishop says

Great technique, and great to see it in action. One problem I do have with it though is that as it stands, when you drag and drop and /then/ upload, you confirm that this is the right file, and that you’re ready to upload it.

With what you’re showing, there is no ‘confirm’ step which I think is a safeguard people are used to having. Nonetheless, impressive.

@_dhar says

@Russell: Most often, if the user realizes he choose a wrong picture, he just needs to upload another one the replace the old one (or click a remove button). Think of a profile pict for example.
I feel that in many situations, confirmation step is pretty useless. And if you really need a confirmation, it’s up to you to add a submit button ;)

andrei says

When appending files to the FormData variable shouldn’t it be
>>> formData.append(‘file’ + i, files[i]); ?

Greg says

Remy,

Totally not intending to advertise for my company’s commercial offering (longtime reader of yours, just happened to see this due to a Question at Stack Overflow) and if I seem pushy or this comment is out of place I won’t be at all offended if you don’t approve it. But all of this reminds me why despite emerging technologies, we (Unlimi-Tech Software) continue to have success with our Java Applets including our uploaders (U-Upload).

A script tag and it’s in your page ready for drag-and-drop, or you can use a headless scriptable version and your own bespoke drag-and-drop widget.

The bummer is that Java SE applets aren’t supported for mobile devices (yet… and probably never!). But the benefit is a crazy simple API and the ability to connect to an FTP server.

If I had a site that relied on transfers as part of a commercial offering I would be tempted to go the opposite way in terms of fallbacks (vs. what was proposed in part of this article)– check for Java and use the applet, and if not then try to fall back to something based on HTML5 (which will likely be mobile).

In any event, all interesting stuff! Thanks for the step-by-step and for documenting the pitfalls you faced (all the better for learning!) Gets the brain turning on the different ways to accomplish file transfers from the web.

Phil Parsons says

It’s funny that not a great deal has changed in the spec since I wrote the article you mentioned back in 2010.

To reiterate what Paco has said, you would be better creating the data URI with furl = createObjectURL(file) and then clean that up with revokeObjectURL(furl) once it is no longer needed.

I have written a small Ajax library (https://github.com/p-m-p/xhr2-lib) that supports uploading files and forms containing files for anyone who may want a higher level API to do this stuff.

Gareth says

@Greg While you may have better support for a
java applet, I think it’s the wrong decision to use that as the default. We should always be throwing the web standards approach first and falling back to the proprietary technologies.

This can also be done in Flash, which is probably even more supported but again, checking for support of the standard and falling back to flash is the way we should be heading.

We see this pattern with many of the forward thinking video players such as http://videojs.com/ – they will all check support for HTML5 video and fall back to flash if supported.

Greg says

Gareth,

There’s definitely something to what you say… in principle! If you’re implementing a fallback one way or another, the question of coding ability becomes a moot point… primary… secondary… you’re still writing the code.

But the reason I suggested the order of fallback wasn’t due to preference for Java (I’m all for native when it makes sense) or the hurdle of coding ability, though; rather it was about availability of features.

Without going into much detail (I did, and then just deleted it before hitting submit because it was wordy and boring!) the file transfer API for HTML5 doesn’t even scratch the surface of what might be needed by an organization that has file transfer as a critical part of their business. It might get there some day… that would be nice! But in the same way that our clients use HTTP transfers as a fallback, they will be more likely to use the new API as fallback also.

I can see a day when some of our products switch to being native-first. The uploader that we’re talking about would probably be one of the first. Other applets like our two-way applet and our UDP-based transfer applets will probably not be able to have native versions within HTML5′s lifetime. Only time will tell!

A brief note regarding availability: Java is far more present (95% ish) on the desktop than Flash (75% ish). On mobile, neither are good choices. But that’s what the fallback would be for. ;-)

zet says

i’d rather use these steps :
1-Capture the drop event and read its data.
2-render a preview
3-scale and crop image (https://github.com/can3p/iviewer)
4-Post scaled and cropped binary data to the server.
5-Provide feedback on the progress of the upload.

thus you wont upload big image on your server ,the upload will time will be shorter and you save server’s disk space and cpu.

regards

Remy Sharp says

@zet – it really depends on what your application does, doesn’t it? What if the user is uploading txt files or pdfs?

And those steps are definitely possible, when you render, copy it in to a canvas and add cropping tools. Once done, use `drawImage` cropping the data in to a temp canvas and export to an image and send it quietly up to the server.

Remember that disk space and cpu usage on the *server* is so much less important than the client. Because you’re machine can be beefy as heck, whilst you want your user to easily continue on with their job – not that any of these tasks described would consume anything short of a tiny amount of resource both on the client or server.

Michael Butler says

Hi Remy, I’ve been using Drag n drop upload for a while in Firefox & Chrome, sometimes updating the code with the API changes.
Now it seems drag ‘n drop upload is broken in Firefox 11 (I’m on Ubuntu 11.10).

I’ve tracked the problem down in Firebug down to the e.dataTransfer.files property on the onDrop event. For some reason, the “files” object always has a length of zero, no files, so it just skips out.

Everything still works in Chrome. I’ve tried other examples on the web and even Gmail’s drop to attach file with my Firefox 11 and none of them seem to work. Just wondering if it’s a Firefox bug or something wrong with my install.

Thanks.

Mac says

@Michael Butler … DnD is working fine in FF11 on Windows.

Unfortunately XHR2 doesn’t appear to be available yet. Admittedly I’m running my POST through jQuery’s ajax function, but an attempt to point xhr.upload.onprogress to a handler throws an error.

I’m also a bit unhappy with the fact that FileReader doesn’t seem to make more than one or two calls to the progress event even for multi-megabyte files… unless I’m throwing around hundreds of megs (which isn’t in my use-case) spending time on progress indicators seems to be a waste of time (even though a 2MB or 3MB file *is* in the use-case and takes enough time that progress updates would be useful).

Remy Sharp says

@Mac – I suspect your usage is off. I’m making a couple of assumptions, so firstly, using jQuery’s Ajax function doesn’t give you access to the XHR object. It creates a bespoke jqXHR object: http://api.jquery.com/jQuery.ajax/#jqXHR – this is a superset of XHR, but my gut tells me not everything is there. If you’re having an error thrown on adding a handler to onprogress I suspect it’s because the upload property is undefined – which supports my suspicion that you don’t actually have the real XHR object.

Secondly, on the FileReader firing events often enough – again I suspect you’ve not hooked it properly (and I had the same problem the first few times around – the progress would appear to jump way too quickly).

If you’re not able to hook the progress event on the upload property then it shouldn’t even update the progress indicator. Just for you own sanity and mine, I’ve captured a screencast showing progress on a 15mb file and an 800k file so you can see the speed in which the progress updates (this is on a regular 8mb+ broadband connection): http://www.screenr.com/DTg8

Keep playing, it’s worth it once the hooks are properly in place.

Ankkit says

I wanted to know if the ‘server’ can be the public folder in your dropbox account? Also in this example how do I direct it to a specific server adress?

Bjorn Roche says

This would be a bit more user-friendly if you implemented the ondragleave handler:

holder.ondragleave = function () { this.className = ”; return false; };

kartoshin says

Hello and thank you for this helpful article.

I was using your demo code to upload several files. Though it showed all of them in browser, it sent only one file to the script. In order to upload several files we should replace

formData.append('file', files[i]);

with

formData.append('file[]', files[i]);

Daniel says

Kartoshin – Thank you for your comment. I forgot about this so your comment helped me. :)

Julian says

I tested this software, and it worked beautifully, BUT, as poster Kartoshin pointed out, you need square brackets in:

formData.append(‘file[]‘, files[i]);

rather than: formData.append(‘file’, files[i]);

THANK YOU, Remy, for all your fine work! :D

————-
To the poster who was asking how to specify the location on the server, do the following:

1. Start with the source code in Remy’s example, at http://html5demos.com/dnd-upload

2. Replace xhr.open(‘POST’, ‘/devnull.php’);
with:
xhr.open(‘POST’, ‘http://YourDomain/YourScript.php’);

where “YourScript.php” is a script on your server that you need to provide. This script will receive the uploaded files — and it’s in this script that you provide the information about where in the server to store them.

3. Your script should have a line of the type:

$uploadedFiles = $_FILES["file"];

where the name “file” must be the string used in formData.append(‘file[]‘, files[i]).

(Careful not to mix up the singular with the plural – poor choice of similar variable names!)

4. All left to do in your script is the same as if you were doing a “regular” multiple file upload, i.e. the kind you would do with .

Essentially, looping over the arrays inside the variable $uploadedFiles , and moving the uploaded files from your server’s temporary folder to your desired destination – for example by means of the PHP function move_uploaded_file

Julian says

Oops, in my line 4, above, I meant to say:
the kind you would do with &ltINPUT type=’file’ name=’file[]‘>

Julian says

Here is the function I use for the multiple uploads. You invoke it as:
uploadMultipleFiles($uploadedFiles , “full_destination_path/”):


function uploadMultipleFiles($upload_fileset_info, $dest_dir, $verbose = false)
/* Attempt to upload multiple files to the specified destination directory (which should end with a "/"). The file names are not altered.
*/
{
$number_successful_uploads = 0;

// Loop over the "error" array, and extract the corresponding info (with the same index offset) in the other 4 arrays
foreach ($upload_fileset_info["error"] as $index => $status_code) {
$name = $upload_fileset_info["name"][$index];
if ($name == "") {
continue; // If no upload file was given, skip to the next entry
}

$type = $upload_fileset_info["type"][$index];
$tmp_name = $upload_fileset_info["tmp_name"][$index];
$size = $upload_fileset_info["size"][$index];

if ($status_code == UPLOAD_ERR_OK) { // If upload was successful

$dest_filename = $name;

$dest_full_path = $dest_dir . $dest_filename;

// Move the temporary upload file to its desired destination on the server
move_uploaded_file($tmp_name, "$dest_full_path") or die("ERROR: Couldn't copy the file to the desired destination.");

$number_successful_uploads += 1;
}
else { // If upload didn't succeed, spell out the nature of the problem
echo "Upload FAILED";
}
}

return $number_successful_uploads;
}

Corey says

Julian
Thank you for your extra code provided, it’s exactly what I needed as someone who doesn’t know much about php and especially file uploads.

I was struggling with getting your code to work though, and I finally got it to work by changing:
move_uploaded_file($tmp_name, "$dest_full_path")
to
move_uploaded_file($tmp_name, $dest_full_path)
Honestly not sure if this is how you are supposed to do it, but it works for me.

Barney says

@Remy regarding jQuery ajax compatibility, it is all there, it’s just that it makes a bunch of assumptions that normally make life easier but block this use case. You can easily fix this without hacking it though, namely by adding these properties to your $.ajax() call:


// Ensures jQuery does not force its own HTTP header, preserving multipart/form content-type
contentType : false,
// Stop it from attempting to parse the data on its way out.
processData : false

…So the construction + call would look something like this:


var formData = new FormData();

formData.append('whatevs', yourFile);

$.ajax({
contentType : false,
data : formData,
processData : false,
success : ensured(),
type: 'POST',
url : '/destination'
})

Paul says

@ Corey/Julian

Thank you for your insight as to how to get file upload working to specify the location on the server. Im new to PHP and my problems have come when creating the script, ive played around with it for the past few days but cant seem to get it working?

I was wondering whether the placing of:

$uploadedFiles = $_FILES["file"];

within the script in relation to the above function could be the problem?

Any help with this would be greatly appreciated as its driving me nuts at the moment :)

Thanks in advance!

Darius says

Im getting an error at line 2


$uploadedFiles = "";
foreach ($_FILES["file"]["error"] as $key => $error) &lt
if ($error == UPLOAD_ERR_OK) &lt
$name = $_FILES["file"]["name"][$key];
$uploadedFiles.=$name;
move_uploaded_file( $_FILES["file"]["tmp_name"][$key], "uploads/" . $_FILES['file']['name'][$key]);
>
>

its seems $_FILES["file"] is empty, but im adding the files to the formData

for (var i = 0; i < files.length; i++) &lt
formData.append('file', files[i]);
>

files array has one image in it.

Any Ideas?

Guillaume says

Same question than Darius :s someone to help?

Darius says

Try it like this:

foreach ($_FILES as $props) {
$error = $props[error];
if ($error == UPLOAD_ERR_OK) {
$name = $props[name];

So asking for the array without the ” ”

Darius

Guillaume says

Thanks for the fast answer … ok now I’m lost ;) could you compile me the upload code in full please. I really don’t manage to get it work.
The post request is done but there is nothing uploaded in the folder.
Thanks for the help

Greg says

Wanted to pop in and thank you for a great start you gave me; I took it a different direction and thought that your readers might be interested:

- The files list is inside the event, so if you don’t need to pass it into formData, you can post each individual file and get information from each file’s xhr2 object. This will allow you to do things like per-file progress updates. So, instead of pushing into the formData object, you can just use the event directly (in the case of a change event, it is event.target.files).

- The “fallback” to the file input button is a great idea. We decided to do something like this, but instead of “one or the other”, we always provide the input node. Only the drop area is optional and available on detection.

To streamline everything, we therefore only trigger the upload from a “change” event on the input node. The drop widget takes the files object and throws it over to the input node (document.getElementById(‘files-upload’).files = event.originalEvent.dataTransfer.files;), which triggers the change event.

Hope that’s some sort of benefit to someone!

Chintan Patel says

Hi Remy,

I am working on java – spring mvc. I am submitting my data through JSON.

What I want to do is:
I want to upload an image instantly in to temporary folder on my server using , and show what is to be uploaded.

On final submit all data including uploaded image path (temporary folder’ path) will be passed to my spring controller through JSON object, then transfer image from temporary directory to actual directory & saved all data in to a database.

There are few questions:
1. Where the file is to be uploaded (location)?
2. Where to give folder path?

I would be appreciated if you can hep me.

Thanks in advance.

Hershel Pearl says

Remy,

Could I use this to D&D routine to drag (copy or cut(?)) a record that was acquired from the database, from a column to another column, then with a webmethod postback the update/change to the server’s database, then update the client with the results?

Yohn says

for multiple file upload I changed one of your lines in the javascript -> formData.append(‘file[]‘, files[i]); <- added the [] in the statement and I was able to get the multiple file uploads to work correctly :) Thanks for the script! its awesome

Ian Yang says

I used the php script here, but can’t see the uploaded file in the /upload folder.

I changed XHR request to upload.php in the js code. And the progress can go to 100%. But I just don’t see the uploaded image in /upload folder.

Could anyone help? Thanks.

Ian Yang says

It turned out to be the file size limit in the php script which caused the issue. Thanks all. No action needed.

Aish says

Sir,
Please tell me how to arrange images horizontally when we r dragging them.

In example they are getting arranged vertically.

I wan to make horizontal tile.

Join the discussion.

Some HTML is ok

You can use these tags:
<a href="" title="">
<abbr title="">
<b>
<blockquote cite="">
<cite>
<del datetime="">
<em>
<i>
<q cite="">
<strong>

You can also use <code>, and remember to use &lt; and &gt; for brackets.