DEV Community

Cover image for Drag and Drop File Upload in Vue.js
Raymond Camden
Raymond Camden

Posted on • Originally published at raymondcamden.com on

Drag and Drop File Upload in Vue.js

This won’t be a terribly long post. I had to build a small demo for a friend demonstrating drag/drop along with uploading so I thought I’d share the code for others. Honestly this is mostly for me so that when I need to build this again in a few months I’ll Google and end up back here completely surprised that I had already written it.

I’ll start off by saying I’m not going to cover the mechanics of drag and drop here. The MDN Web Docs have a great article on this (of course they do): HTML Drag and Drop API. In my case, I’m not concerned with making a DOM item dragable but rather making my code respond to drop events.

For what I need I have to handle two events, drop and dragover. Handling drop makes sense. I’ll be honest and say I’m not quite sure why I need to handle dragover, but the code is incredibly small as you just need to prevent the default behavior.

Working on this demo also taught me something else about Vue. I’m used to building my Vue apps like so:

<div id="app">
Here is where the awesome happens...
</div>

Enter fullscreen mode Exit fullscreen mode

Where my div is then passed to Vue:

const app = new Vue({
    el:'#app',
    //lots more stuff here
});

Enter fullscreen mode Exit fullscreen mode

However, what if I wanted to do something with <div id="app"> app itself? Turns out you can add Vue directives there just fine. I guess that makes sense but I’d never tried that before. I was able to specify that my entire Vue application “area” was covered by drag and drop support.

Ok with that out of the way, let’s look at the code. I’ll start off wth HTML.

<html>
<body>
<div id="app" v-cloak @drop.prevent="addFile" @dragover.prevent>
  <h2>Files to Upload (Drag them over)</h2>
  <ul>
    <li v-for="file in files">
      {{ file.name }} ({{ file.size | kb }} kb) <button @click="removeFile(file)" title="Remove">X</button>
    </li>
  </ul>

  <button :disabled="uploadDisabled" @click="upload">Upload</button>
</div>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode

On top, you can see my two event handlers. As I said, for dragover all we need to do is prevent default behavior which makes that part short and sweet. The drop event, addFile, is where I’ll handle generating the list of files.

Inside the div I keep track of the files you want to upload. For each I output the name, the size (passed through a filter kb), and add a simple button to let you remove the item.

Finally I’ve got an button to fire off the upload. For my demo I don’t bother using a “Loading” widget of any sort, nor do I clear out the files when done. If anyone wants to see that just ask!

Alright, now the code.

Vue.config.productionTip = false;
Vue.config.devtools = false;

Vue.filter('kb', val => {
  return Math.floor(val/1024);  
});

const app = new Vue({
  el:'#app', 
  data: {
    files:[]
  },
  computed: {
    uploadDisabled() {
      return this.files.length === 0;
    }
  },
  methods:{
    addFile(e) {
      let droppedFiles = e.dataTransfer.files;
      if(!droppedFiles) return;
      // this tip, convert FileList to array, credit: https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/
      ([...droppedFiles]).forEach(f => {
        this.files.push(f);
      });
    },
    removeFile(file){
      this.files = this.files.filter(f => {
        return f != file;
      });      
    },
    upload() {

      let formData = new FormData();
      this.files.forEach((f,x) => {
        formData.append('file'+(x+1), f);
      });

      fetch('https://httpbin.org/post', {
        method:'POST',
        body: formData
      })
      .then(res => res.json())
      .then(res => {
         console.log('done uploading', res);
      })
      .catch(e => {
        console.error(JSON.stringify(e.message));
      });

    }
  }
})

Enter fullscreen mode Exit fullscreen mode

On top you can see my simple kb filter to render the file sizes a bit nicer. Inside the Vue app I’ve got one data item, files, and note how uploadDisabled works as a nice computed property.

In addFile, I use the Drag/Drop API to access the files (if any) that were dropped. This demo lets you drag over one file, or 100 (don’t do that). I then iterate over each and add them to the files value. Remember that when a user intentionally provides a file to a web app you now have read access to it. That’s how I’m able to show the file sizes. I could do a lot more here like validate file type, set a max size per file, or even set a total size allowed.

Finally, my upload method just hits httpbin.org which will echo back what it was sent. I create a FormData object and just append each file. Remember by the user dropping the files on the app we can read from them.

And that’s it. I hope this simple demo helps!

Header photo by Jimmy Chang on Unsplash

Top comments (5)

Collapse
 
jdchmiel profile image
Jared Chmielecki

Is dragover where the cursor is changed to the add file indicator?

Collapse
 
raymondcamden profile image
Raymond Camden

I think so. The MDN docs I linked to don't actually provide a lot of detail (I think, let me know if I'm missing it). Normally they are super verbose but on this I'm not quite getting it.

Collapse
 
ehutch79 profile image
Eric Hutchinson

This is awesome. A lot of libraries that support this functionality are several K and still require this much code.

Collapse
 
mgasparel profile image
Mike Gasparelli

Thanks for taking the time to share this more widely!

Don't forget to set the Content-Type header to multipart/form-data!

Collapse
 
raymondcamden profile image
Raymond Camden

I just checked in the CodePen (which I somehow forgot to share - codepen.io/cfjedimaster/pen/ymvMpj) - and it looks like Fetch set it automatically. (I totally didn't know that though.)