** How To Make A Drag-and-Drop File Uploader With Vanilla JavaScript by Joseph Zimmerman

../../../../_images/joe-zimmerman-profile.jpg

Joseph Zimmerman

Introduction

It’s a known fact that file selection inputs are difficult to style the way developers want to, so many simply hide it and create a button that opens the file selection dialog instead.

Nowadays, though, we have an even fancier way of handling file selection: drag and drop .

Technically, this was already possible because most (if not all) implementations of the file selection input allowed you to drag files over it to select them, but this requires you to actually show the file element.

So, let’s actually use the APIs given to us by the browser to implement a drag-and-drop file selector and uploader.

In this article, we’ll be using “vanilla” ES2015+ JavaScript (no frameworks or libraries) to complete this project, and it is assumed that you have a working knowledge of JavaScript in the browser.

This example — aside from the ES2015+ syntax, which can easily changed to ES5 syntax or transpiled by Babel — should be compatible with every evergreen browser plus IE 10 and 11.

Setting Up Our Form

Before we start adding drag-and-drop functionality, we’ll need a basic form with a standard file input.

Technically this isn’t necessary, but it’s a good idea to provide it as an alternative in case the user has a browser without support for the drag-and-drop API.

form

<div id="drop-area">
  <form class="my-form">
    <p>Upload multiple files with the file dialog or by dragging and dropping images onto the dashed region</p>
    <input type="file" id="fileElem" multiple accept="image/*" onchange="handleFiles(this.files)">
    <label class="button" for="fileElem">Select some files</label>
  </form>
</div>

function handleFiles + uploadFile

function handleFiles(files) {
  files = [...files]
  initializeProgress(files.length) // <- Add this line
  files.forEach(uploadFile)
  files.forEach(previewFile)
}

function uploadFile(file) {
  let url = 'YOUR URL HERE'
  let formData = new FormData()

  formData.append('file', file)

  fetch(url, {
    method: 'POST',
    body: formData
  })
  .then(progressDone) // <- Add `progressDone` call here
  .catch(() => { /* Error. Inform the user */ })
}

Here we use FormData, a built-in browser API for creating form data to send to the server .

We then use the fetch API to actually send the image to the server .

Make sure you change the URL to work with your back-end or service, and formData.append any additional form data you may need to give the server all the information it needs

Additional Features

That is all of the base functionality, but often we want more functionality.

Specifically, in this tutorial, we’ll be adding a preview pane that displays all the chosen images to the user, then we’ll add a progress bar that lets the user see the progress of the uploads.

So, let’s get started with previewing images.

Image Preview

There are a couple of ways you could do this: you could wait until after the image has been uploaded and ask the server to send the URL of the image, but that means you need to wait and images can be pretty large sometimes.

The alternative — which we’ll be exploring today — is to use the FileReader API on the file data we received from the drop event.

This is asynchronous, and you could alternatively use FileReaderSync, but we could be trying to read several large files in a row, so this could block the thread for quite a while and really ruin the experience.

So let’s create a previewFile function and see how it works:

function previewFile(file)

function previewFile(file) {
  let reader = new FileReader()
  reader.readAsDataURL(file)
  reader.onloadend = function() {
    let img = document.createElement('img')
    img.src = reader.result
    document.getElementById('gallery').appendChild(img)
  }
}

Here we create a new FileReader and call readAsDataURL on it with the File object.

As mentioned, this is asynchronous, so we need to add an onloadend event handler in order to get the result of the read.

We then use the base 64 data URL as the src for a new image element and add it to the gallery element.

There are only two things that need to be done to make this work now: add the gallery element, and make sure previewFile is actually called.

First, add the following HTML right after the end of the form tag:

<div id="gallery"></div>

Nothing special; it’s just a div. The styles are already specified for it and the images in it, so there’s nothing left to do there.

function handleFiles

Now let’s change the handleFiles function to the following:

function handleFiles(files) {
  files = [...files]
  files.forEach(uploadFile)
  files.forEach(previewFile)
}

There are a few ways you could have done this, such as composition, or a single callback to forEach that ran uploadFile and previewFile in it, but this works too.

And with that, when you drop or select some images, they should show up almost instantly below the form.

The interesting thing about this is that — in certain applications — you may not actually want to upload images, but instead store the data URLs of them in localStorage or some other client-side cache to be accessed by the app later.

I can’t personally think of any good use cases for this, but I’m willing to bet there are some.

Tracking Progress

If something might take a while, a progress bar can help a user realize progress is actually being made and give an indication of how long it will take to be completed.

Adding a progress indicator is pretty easy thanks to the HTML5 progress tag. Let’s start by adding that to the HTML code this time.

<progress id="progress-bar" max=100 value=0></progress>

You can plop that in right after the label or between the form and gallery div, whichever you fancy more. For that matter, you can place it wherever you want within the body tags.

No styles were added for this example, so it will show the browser’s default implementation, which is serviceable.

Now let’s work on adding the JavaScript.

We’ll first look at the implementation using fetch and then we’ll show a version for XMLHttpRequest.

To start, we’ll need a couple of new variables at the top of the script

let filesDone = 0
let filesToDo = 0
let progressBar = document.getElementById('progress-bar')

When using fetch we’re only able to determine when an upload is finished, so the only information we track is how many files are selected to upload (as filesToDo) and the number of files that have finished uploading (as filesDone).

function progressDone() and initializeProgress(numfiles)

We’re also keeping a reference to the #progress-bar element so we can update it quickly. Now let’s create a couple of functions for managing the progress:

function initializeProgress(numfiles) {
  progressBar.value = 0
  filesDone = 0
  filesToDo = numfiles
}

function progressDone() {
  filesDone++
  progressBar.value = filesDone / filesToDo * 100
}

When we start uploading, initializeProgress will be called to reset the progress bar.

Then, with each completed upload, we’ll call progressDone to increment the number of completed uploads and update the progress bar to show the current progress.

So let’s call these functions by updating a couple of old functions:

function uploadFile

function handleFiles(files) {
  files = [...files]
  initializeProgress(files.length) // <- Add this line
  files.forEach(uploadFile)
  files.forEach(previewFile)
}

function uploadFile(file) {
  let url = 'YOUR URL HERE'
  let formData = new FormData()

  formData.append('file', file)

  fetch(url, {
    method: 'POST',
    body: formData
  })
  .then(progressDone) // <- Add `progressDone` call here
  .catch(() => { /* Error. Inform the user */ })
}

Conclusion

That’s the final piece. You now have a web page where you can upload images via drag and drop, preview the images being uploaded immediately, and see the progress of the upload in a progress bar.

You can see the final version (with XMLHttpRequest) in action on CodePen , but be aware that the service I upload the files to has limits, so if a lot of people test it out, it may break for a time.