Drag and Drop File Uploads with VueJS and Axios

Part 3 of 7 in Your Guide To Uploading Files with VueJS and Axios
Dan Pastori avatar
Dan Pastori January 9th, 2018

When it comes to uploading files for the web, dragging and dropping files from a directory is one of the most convenient ways to accomplish this task. Almost any modern web application allows you to do this. Up until this point, we have all the different tricks to upload files with VueJS and Axios. We have standard file uploads where users can do a single file, multiple files, and even edit the files before submitted: Uploading Files With VueJS and Axios – Server Side Up, we have file previews: Preview File Uploads with Axios and VueJS, and we have a status bar of the upload process: File Upload Progress Indicator with Axios and VueJS .

In this tutorial we are going to combine all of these tricks to make the ultimate file uploader with VueJS and Axios. We will allow users to select any amount of files, remove the ones they don’t, show a preview, and show a status bar for uploading the files. The user will be able to select which files they want through dragging and dropping or selecting through the standard uploader.

A lot of the drag and drop functionality comes from Osvaldas Valutis and his guest post on CSS tricks: Drag and Drop File Uploading | CSS-Tricks. Mad props to Osvaldas, the tutorial was the best I’ve seen. I’ve adapted what we need to be inside a Vue component and not with jQuery and added some of the bells and whistles from the last couple articles.

This tutorial will flow in steps, we will:
1. Build our drag and drop base.
2. Add previews for the files selected if they are images
3. Allow users to remove the files they don’t want any more
4. Upload the selected files
5. Add a progress bar
6. Take a short derivative for the direct to upload process. This will upload the files when they are dropped onto the drop base.

Lots of cool stuff in this tutorial, but it can get a little complicated at times so reach out if you need a hand during any of the steps!

🚨 UPDATE 10/14/2021

We’ve created a Github repo that contains a functioning VueJS component for both drag and drop and also an instant drag and drop. Both of these components have been updated for Vue 3.0 and Axios 0.21.1. There are also a few significant structural changes, such as removing the $refs attribute and dividing up the functionality into more maintainable methods. Definitely recommend heading over to Github and checking out these two components!

Building Your Drag And Drop Base

This is the guts of the tutorial, but also where we should start. The key to having a drag and drop file uploader is to have an element where you can drag and drop files.

First, we need to create an empty Vue Component. In this component add the following component stub:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
      border-radius: 4px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
        <span class="drop-files">Drop the files here!</span>
    </form>
  </div>
</template>

<script>
  export default {

  }
</script>

In here is where we will build all of our functionality. I added a real basic style for the form which just centers it in the screen and added some text to instruct the users where to drag and drop the files. Osvaldas has a beautiful UX in his tutorial here: Drag and Drop File Uploading | CSS-Tricks. I’ll be focusing just on porting some of the functionality to VueJS and Axios. Feel free to style the elements anyway you’d like!

The only real thing to point about about the template is the ref attribute on the <form> tag. This allows us to access the element from within our component code.

Confirm that browser is drag and drop capable

The first piece of functionality we will add to our component is a check to see if the drag and drop functionality is supported or not. Older browsers don’t support drag and drop so we will allow for a backup plan with the users clicking a button.

First, let’s add a data field to our component with a flag to see if the browser is drag and drop capable:

/*
  Variables used by the drag and drop component
*/
data(){
  return {
    dragAndDropCapable: false,
      files: []
  }
},

This flag will determine if we show the text to allow drag and drop and if we even bind the events to the elements. We also added an array of files that will store the files the user selected when they dragged and dropped over the element.

Next, we will want to add a method that determines whether the browser is drag and drop capable. Add the following method to your methods object in the Vue component:

 /*
  Determines if the drag and drop functionality is in the
  window
*/
determineDragAndDropCapable(){
  /*
    Create a test element to see if certain events
    are present that let us do drag and drop.
  */
  var div = document.createElement('div');

  /*
    Check to see if the `draggable` event is in the element
    or the `ondragstart` and `ondrop` events are in the element. If
    they are, then we have what we need for dragging and dropping files.

    We also check to see if the window has `FormData` and `FileReader` objects
    present so we can do our AJAX uploading
  */
  return ( ( 'draggable' in div )
          || ( 'ondragstart' in div && 'ondrop' in div ) )
          && 'FormData' in window
          && 'FileReader' in window;
}

To step through this, we first create a test div element where we can see if certain events are available. These events allow us to listen to the drag and drop events from the files.

We also return if the FormData (FormData – Web APIs | MDN) and FileReader (FileReader – Web APIs | MDN) objects are present in the window for full functionality. If these all exist, we will return true from the method and continue the initialization.

Initialize our component through the mounted() lifecycle hook

We will be using the functionality from the determineDragAndDropCapable() method in the mounted() lifecycle hook to determine whether or not we should add the event listeners.

Let’s first add our lifecycle hook and initializers like so:

mounted(){
  /*
    Determine if drag and drop functionality is capable in the browser
  */
  this.dragAndDropCapable = this.determineDragAndDropCapable();

  /*
    If drag and drop capable, then we continue to bind events to our elements.
  */
  if( this.dragAndDropCapable ){
    /*
      Listen to all of the drag events and bind an event listener to each
      for the fileform.
    */
    ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
      /*
        For each event add an event listener that prevents the default action
        (opening the file in the browser) and stop the propagation of the event (so
        no other elements open the file in the browser)
      */
      this.$refs.fileform.addEventListener(evt, function(e){
        e.preventDefault();
        e.stopPropagation();
      }.bind(this), false);
    }.bind(this));

    /*
      Add an event listener for drop to the form
    */
    this.$refs.fileform.addEventListener('drop', function(e){
      /*
        Capture the files from the drop event and add them to our local files
        array.
      */
      for( let i = 0; i < e.dataTransfer.files.length; i++ ){
        this.files.push( e.dataTransfer.files[i] );
      }
    }.bind(this));
  }
},

The most important reason to use the mounted() hook is it runs after the template is injected into the HTML. This allows us to access the reference to the form that we created when we created our template.

First, we set our local dragAndDropCapable flag to what is returned from our method:

/*
  Determine if drag and drop functionality is capable in the browser
*/
this.dragAndDropCapable = this.determineDragAndDropCapable();

This determines if we should continue binding the events we are looking for on the form field or not. No sense binding events that don’t exist!

After we determine that drag and drop functionality is capable, we iterate over all of the drag related events and bind them to our form:

/*
  Listen to all of the drag events and bind an event listener to each
  for the fileform.
*/
['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
  /*
    For each event add an event listener that prevents the default action
    (opening the file in the browser) and stop the propagation of the event (so
    no other elements open the file in the browser)
  */
  this.$refs.fileform.addEventListener(evt, function(e){
    e.preventDefault();
    e.stopPropagation();
  }.bind(this), false);
}.bind(this));

What we do here is iterate over an array of the drag related events. We then bind an event listener to each event on the form we access with our global $refs array. What we simply do with the events is prevent the default action which is to open the files dragged into the browser in a new tag. And then we stop the propagation of the event so no other elements decided they will open the files in a new tab.

Now we need to actually listen to an event to detect the user’s files they are dropping. To do this, we need to add an event listener to the drop event on the form:

/*
  Add an event listener for drop to the form
*/
this.$refs.fileform.addEventListener('drop', function(e){
  /*
    Capture the files from the drop event and add them to our local files
    array.
  */
  for( let i = 0; i < e.dataTransfer.files.length; i++ ){
    this.files.push( e.dataTransfer.files[i] );
  }
}.bind(this));

There are a few features to point out with this event listener. First, we bind the local component with the .bind(this) method to the function we are handling the drop event with. This gives us the capability to reference the component directly and set local parameters.

Next, we iterate over all of the files being transferred. A user can select multiple files and drag/drop them onto the form. We need to grab all of these files and iterate over them adding the files to the local files array. To access the files, we reference the e.dataTransfer.files from the event to grab the files we need. This is what we iterate over and add each to the local files array. Now we can use these later on to send to our server! Each file is represented by the File object: File – Web APIs | MDN, which plays nicely when we send the file with FormData and axios.

This is the base for what we need for drag and drop file uploading! We will be adding a few bells and whistles kind of as a capstone for the other tutorials, but also as an example of what you can do with VueJS and Axios for file uploading.

Add Previews For Files Selected (if the files selected are image)

So now that we have the ability to drag and drop files to upload, let’s preview the files if they are images, or have a placeholder if they aren’t. This will be a nice UX feature just to see what they have selected. We will then allow the users to remove the files they don’t want to upload before continuing.

Display Files After Drag and Drop

The first step in the process will be to display the files selected by the user. We will need to iterate over the files array and display a simple preview if the file is an image.

In our template, right below the form, add the following markup:

<div v-for="(file, key) in files" class="file-listing">
      <img class="preview" v-bind:ref="'preview'+parseInt( key )"/>
      {{ file.name }}
</div>

What this does is iterates over all of the files in the files array making a simple preview template. In our template, the first element is an <img> tag that binds a reference to the preview and the key of the file in the files array. This is so we can set the src attribute through code if it is an image. We will be doing this in the next step.

The other piece of the template is just the name of the file provided by the user.

For a little bit of added style (so it’s not a complete disarray of elements) I added the following CSS to the <style> tag for the component:

div.file-listing{
  width: 400px;
  margin: auto;
  padding: 10px;
  border-bottom: 1px solid #ddd;
}

div.file-listing img{
  height: 100px;
}

This will give us a nice row to display what each file is and allow us to add a button to remove the file if it’s no longer needed.

Implement File Preview if image

The most important feature of this step is to allow a file preview if the file is an image, otherwise show a default file. As I mentioned in the article: Preview File Uploads with Axios and VueJS, I’m sure there are other file types that can be previewed successfully, but for the purpose of this tutorial, we will just be implementing it with images.

The first line of code we add is in the event handler for drop. When fired, after we add the files to the local file array, we need to add this code:

this.getImagePreviews();

We will be implementing this method to display the file previews, but for now, we just need to call it in our event handler. This method will display the image preview or a placeholder image if the filetype is not an image.

Now, we need to add the method to the methods array in our component:

/*
  Gets the image preview for the file.
*/
getImagePreviews(){
  /*
    Iterate over all of the files and generate an image preview for each one.
  */
  for( let i = 0; i < this.files.length; i++ ){
    /*
      Ensure the file is an image file
    */
    if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) {
      /*
        Create a new FileReader object
      */
      let reader = new FileReader();

      /*
        Add an event listener for when the file has been loaded
        to update the src on the file preview.
      */
      reader.addEventListener("load", function(){
        this.$refs['preview'+parseInt( i )][0].src = reader.result;
      }.bind(this), false);

      /*
        Read the data for the file in through the reader. When it has
        been loaded, we listen to the event propagated and set the image
        src to what was loaded from the reader.
      */
      reader.readAsDataURL( this.files[i] );
    }else{
      /*
        We do the next tick so the reference is bound and we can access it.
      */
      this.$nextTick(function(){
        this.$refs['preview'+parseInt( i )][0].src = '/images/file.png';
      });
    }
  }
}

There’s a lot to step through so let’s get started.

First, we iterate over all of the files stored in the local files array. This will allow us to generate the preview image for each file.

Next we check to see if the file type is that of an image:

if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) {

We do this check because we want to display the actual image as a preview if the file is an image file. Otherwise we have a default icon that we display if the file is not an image.

If the file is an image, we initialize a file reader:

/*
  Create a new FileReader object
*/
let reader = new FileReader();

We then bind a load event to the reader to allowing us to hook in and display an image once it’s been read from the file uploaded by the user:

/*
  Add an event listener for when the file has been loaded
  to update the src on the file preview.
*/
reader.addEventListener("load", function(){
  this.$refs['preview'+parseInt( i )][0].src = reader.result;
}.bind(this), false);

We run a .bind(this) on the event listener we can access our global $refs and set the src of the img tag to the result of the reader which is the blob of the image. Remember, we are scoped inside of a check that the file is an image.

We reference the $ref with the value of i to bind the src to the right image tag. When we generate our template, we base the ref attribute on the key of the file in the files array.

Finally, for images, we read in the image with:

/*
  Read the data for the file in through the reader. When it has
  been loaded, we listen to the event propagated and set the image
  src to what was loaded from the reader.
*/
reader.readAsDataURL( this.files[i] );

It reads in the image in the files array that has the index of i. When the file has been loaded (read) our event fill fire and set the src of the image tag to what was read in.

This works smoothly for images, but for other file types we want just a simple placeholder image. We do that with an else statement after checking for an image.

Since we are scoped outside of the file being an image, we know the file is a different file type. The catcher with this is we have to work inside the $nextTick method within VueJS. Since we are iterating over the files and VueJS is rendering the template, we have to make sure the references are bound so we can set the appropriate attribute on the src tag:

/*
  We do the next tick so the reference is bound and we can access it.
*/
this.$nextTick(function(){
  this.$refs['preview'+parseInt( i )][0].src = '/images/file.png';
});

Now, the template is rendered and we can load the default file of our choice. We didn’t have to wait for the DOM to update on images, because the callback is on load which takes some time so our DOM is updated by the time the file is read in.

This is all we need to do to provide a simple image preview for the files uploaded.

Next, we will allow users to remove files they don’t want before submitting them to the server.

Allow users to remove the files they don’t want any more

This is one of the most important features when uploading multiple files at the same time is the ability to let users remove files that they didn’t want to upload. It can be a little bit tricky and we will be building it similar to what we built here in the first tutorial: Uploading Files With VueJS and Axios – Server Side Up.

Adjust template to allow for removal of files

Right off of the bat, we will need to adjust the template for when we iterate over the files to allow the user to remove a file. In the template add the following code at the end of where we iterate over the uploaded files:

<div class="remove-container">
  <a class="remove" v-on:click="removeFile( key )">Remove</a>
</div>

When clicked, we call the remove( key ) method that we will be implementing. The key is where we will slice the files array and remove the file.

Implement the removeFile( key ) method

The removeFile( key ) method is super simple to implement. Just add the following code to the methods object on your component:

/*
  Removes a select file the user has uploaded
*/
removeFile( key ){
  this.files.splice( key, 1 );
}

This will splice the array at the key passed in from the user. When removed, VueJS will re-render the DOM and the file won’t display. When we submit the files array to the server the file will be removed so it won’t be submitted as well. That’s really all there is too it!

Even though we aren’t focusing much on CSS in this tutorial, I added the following for the remove functionality so it’s easy to click on:

div.remove-container{
  text-align: center;
}

div.remove-container a{
  color: red;
  cursor: pointer;
}

Next up we will be actually making our request to send the files to the server.

Upload the Selected Files

In this step we will actually submit the request with the files to the server. This was covered in the first article I wrote here: Uploading Files With VueJS and Axios – Server Side Up. We will be using the FormData() class provided to do this request. This way we can bundle up the files we need and submit them with Axios to the server.

Add Button

The first step is to add the button to our component that the user can trigger to upload the files. To do this, add the following markup to the template after the display of the files that were selected:

<a class="submit-button" v-on:click="submitFiles()" v-show="files.length > 0">Submit</a>

The first attribute on the template we should note is the v-on:click="submitFiles()" attribute. What this does is when the button is clicked, we call the submitFiles() method. We will be adding this in the next step.

Next, we should note the v-show="files.length > 0" attribute. What this does is only shows the button if there are files uploaded. It’s a simple UX thing, but should be nice for the user.

I also added a few styles for the button:

a.submit-button{
  display: block;
  margin: auto;
  text-align: center;
  width: 200px;
  padding: 10px;
  text-transform: uppercase;
  background-color: #CCC;
  color: white;
  font-weight: bold;
  margin-top: 20px;
}

This just makes it a little easier to use if following along. Now it’s time to add our submitFiles() handler.

Add submitFiles() Handler

In the methods object on your component, add the following method:

/*
  Submits the files to the server
*/
submitFiles(){
  /*
    Initialize the form data
  */
  let formData = new FormData();

  /*
    Iteate over any file sent over appending the files
    to the form data.
  */
  for( var i = 0; i < this.files.length; i++ ){
    let file = this.files[i];

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

  /*
    Make the request to the POST /file-drag-drop URL
  */
  axios.post( '/file-drag-drop',
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      }
    }
  ).then(function(){
    console.log('SUCCESS!!');
  })
  .catch(function(){
    console.log('FAILURE!!');
  });
},

This is the actual magic of the file uploader right here! This is where it submits to the server. Let’s take a quick step through the process.

First, we initialize a FormData() object:

let formData = new FormData();

This allows us to build out our request to send to the server.

Next, up we add each of the files that are in the local files array to the FormData() so we send everything the user has selected:

/*
  Iteate over any file sent over appending the files
  to the form data.
*/
for( var i = 0; i < this.files.length; i++ ){
  let file = this.files[i];

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

We append to the formData that we’ve created making sure each of the files is keyed appropriately.

Now, we make our actual Axios POST request:

/*
  Make the request to the POST /file-drag-drop URL
*/
axios.post( '/file-drag-drop',
  formData,
  {
    headers: {
        'Content-Type': 'multipart/form-data'
    }
  }
).then(function(){
  console.log('SUCCESS!!');
})
.catch(function(){
  console.log('FAILURE!!');
});

The first parameter in the request is the URL that we are submitting the files to. This is just an endpoint I set up on the test server to accept the file request. The second parameter is the formData that contains all of the files that were selected. This would also contain any other inputs that need to be sent along with the files such as text fields, booleans, etc.

The third parameter is the most important in this case. It allows us to configure our request and add extra headers. To send files to the server we need to add the 'Content-Type': 'multipart/form-data' header so the server knows to accept files if needed.

Finally, we hook into a successful request with the .then(function(){}) appended to the request and an unsuccessful request with the .catch(function(){}). These can be used to display messages to your users accordingly. I just have them print to the console right now for testing.

That’s all we need for the actual request. Next, up we will be adding a progress bar to the drag and drop uploader. This will add a little better UX to the interface and can be styled to fit your needs.

Adding a progress bar

This is the last step before we actually submit the files to the server. First, add the markup to the template of your Vue Component to handle the progress bar:

<progress max="100" :value.prop="uploadPercentage"></progress>

I wrote a tutorial about the progress bar here: File Upload Progress Indicator with Axios and VueJS. I’ll quickly go over the features we need to worry about., but for more information check out that article.

The first attribute we should look at is the max attribute. This should be set to 100. What this means is that the progress will be between 0 and 100 which is great since we want to represent the file upload as a percentage.

The second attribute is the property value. Now this is unique since it’s not an input, the value property has to be defined as a property. This way we can have VueJS bind a reactive property to the element so it adjusts accordingly to the status of the component. The value which is bound to this property we will set in the next step.

I added the following styles to the styles tag to make the progress bar a little more usable for the time being:

progress{
  width: 400px;
  margin: auto;
  display: block;
  margin-top: 20px;
  margin-bottom: 20px;
}

Add data to the template

Now that we the template set, let’s add the uploadPercentage value to the data() method in the component like this:

/*
  Variables used by the drag and drop component
*/
data(){
  return {
    dragAndDropCapable: false,
    files: [],
    uploadPercentage: 0
  }
},

We initialize this to 0 so as the percentage increases with the file upload amount, we can increase this as well.

Add listener to file upload request

In the last section we actually made the request with the files to the server. Now, we just need to add one more piece to the configuration for the request and that’s the listener to the upload progress event.

To add this listener in axis add the following to the request configuration (the third parameter):

onUploadProgress: function( progressEvent ) {
  this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) ) );
}.bind(this)

What this does, is listens to the upload progress event and set the local uploadProgress variable to the percentage request’s completion. The event returns how much has been loaded (submitted) and how much needs to be submitted. We can divide the submitted * 100 by the total to get there percentage. This we can set to our local variable which then reactively sets our progress bar to visually represent the completion.

That’s about all we need to do for our drag and drop file uploader. Next up, we will be modifying our drag and drop component to do a direct upload with out a submit button.

For now, our component should look like this and be finalized with the basic amount of features:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
    border-radius: 4px;
  }

  div.file-listing{
    width: 400px;
    margin: auto;
    padding: 10px;
    border-bottom: 1px solid #ddd;
  }

  div.file-listing img{
    height: 100px;
  }

  div.remove-container{
    text-align: center;
  }

  div.remove-container a{
    color: red;
    cursor: pointer;
  }

  a.submit-button{
    display: block;
    margin: auto;
    text-align: center;
    width: 200px;
    padding: 10px;
    text-transform: uppercase;
    background-color: #CCC;
    color: white;
    font-weight: bold;
    margin-top: 20px;
  }

  progress{
    width: 400px;
    margin: auto;
    display: block;
    margin-top: 20px;
    margin-bottom: 20px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
      <span class="drop-files">Drop the files here!</span>
    </form>

    <progress max="100" :value.prop="uploadPercentage"></progress>

    <div v-for="(file, key) in files" class="file-listing">
      <img class="preview" v-bind:ref="'preview'+parseInt( key )"/>
      {{ file.name }}
      <div class="remove-container">
        <a class="remove" v-on:click="removeFile( key )">Remove</a>
      </div>
    </div>

    <a class="submit-button" v-on:click="submitFiles()" v-show="files.length > 0">Submit</a>
  </div>
</template>

<script>
  export default {
    /*
      Variables used by the drag and drop component
    */
    data(){
      return {
        dragAndDropCapable: false,
        files: [],
        uploadPercentage: 0
      }
    },

    mounted(){
      /*
        Determine if drag and drop functionality is capable in the browser
      */
      this.dragAndDropCapable = this.determineDragAndDropCapable();

      /*
        If drag and drop capable, then we continue to bind events to our elements.
      */
      if( this.dragAndDropCapable ){
        /*
          Listen to all of the drag events and bind an event listener to each
          for the fileform.
        */
        ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
          /*
            For each event add an event listener that prevents the default action
            (opening the file in the browser) and stop the propagation of the event (so
            no other elements open the file in the browser)
          */
          this.$refs.fileform.addEventListener(evt, function(e){
            e.preventDefault();
            e.stopPropagation();
          }.bind(this), false);
        }.bind(this));

        /*
          Add an event listener for drop to the form
        */
        this.$refs.fileform.addEventListener('drop', function(e){
          /*
            Capture the files from the drop event and add them to our local files
            array.
          */
          for( let i = 0; i < e.dataTransfer.files.length; i++ ){
            this.files.push( e.dataTransfer.files[i] );
            this.getImagePreviews();
          }
        }.bind(this));
      }
    },

    methods: {
      /*
        Determines if the drag and drop functionality is in the
        window
      */
      determineDragAndDropCapable(){
        /*
          Create a test element to see if certain events
          are present that let us do drag and drop.
        */
        var div = document.createElement('div');

        /*
          Check to see if the `draggable` event is in the element
          or the `ondragstart` and `ondrop` events are in the element. If
          they are, then we have what we need for dragging and dropping files.

          We also check to see if the window has `FormData` and `FileReader` objects
          present so we can do our AJAX uploading
        */
        return ( ( 'draggable' in div )
                || ( 'ondragstart' in div && 'ondrop' in div ) )
                && 'FormData' in window
                && 'FileReader' in window;
      },

      /*
        Gets the image preview for the file.
      */
      getImagePreviews(){
        /*
          Iterate over all of the files and generate an image preview for each one.
        */
        for( let i = 0; i < this.files.length; i++ ){
          /*
            Ensure the file is an image file
          */
          if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) {
            /*
              Create a new FileReader object
            */
            let reader = new FileReader();

            /*
              Add an event listener for when the file has been loaded
              to update the src on the file preview.
            */
            reader.addEventListener("load", function(){
              this.$refs['preview'+parseInt( i )][0].src = reader.result;
            }.bind(this), false);

            /*
              Read the data for the file in through the reader. When it has
              been loaded, we listen to the event propagated and set the image
              src to what was loaded from the reader.
            */
            reader.readAsDataURL( this.files[i] );
          }else{
            /*
              We do the next tick so the reference is bound and we can access it.
            */
            this.$nextTick(function(){
              this.$refs['preview'+parseInt( i )][0].src = '/images/file.png';
            });
          }
        }
      },

      /*
        Submits the files to the server
      */
      submitFiles(){
        /*
          Initialize the form data
        */
        let formData = new FormData();

        /*
          Iteate over any file sent over appending the files
          to the form data.
        */
        for( var i = 0; i < this.files.length; i++ ){
          let file = this.files[i];

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

        /*
          Make the request to the POST /file-drag-drop URL
        */
        axios.post( '/file-drag-drop',
          formData,
          {
            headers: {
                'Content-Type': 'multipart/form-data'
            },
            onUploadProgress: function( progressEvent ) {
              this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
            }.bind(this)
          }
        ).then(function(){
          console.log('SUCCESS!!');
        })
        .catch(function(){
          console.log('FAILURE!!');
        });
      },

      /*
        Removes a select file the user has uploaded
      */
      removeFile( key ){
        this.files.splice( key, 1 );
      }
    }
  }
</script>

Direct Upload without Submit Button

This is a slightly different user experience that may fit the need for some users. Instead of the process being dragging and dropping of files, then verifying what was selected files and pressing submit, this simply uploads all of the files at once when they are dropped on the form.

For this section, I quickly built a new Vue component named FileDragDropInstant.vue and added the functionality we had from our drag and drop base. This will give us most of the functionality we need at the get go.

Our FileDragDropInstant.vue component should begin like this:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
    border-radius: 4px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
      <span class="drop-files">Drop the files here!</span>
    </form>
  </div>
</template>

<script>
  export default {
    /*
      Variables used by the drag and drop component
    */
    data(){
      return {
        dragAndDropCapable: false,
        files: []
      }
    },

    mounted(){
      /*
        Determine if drag and drop functionality is capable in the browser
      */
      this.dragAndDropCapable = this.determineDragAndDropCapable();

      /*
        If drag and drop capable, then we continue to bind events to our elements.
      */
      if( this.dragAndDropCapable ){
        /*
          Listen to all of the drag events and bind an event listener to each
          for the fileform.
        */
        ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
          /*
            For each event add an event listener that prevents the default action
            (opening the file in the browser) and stop the propagation of the event (so
            no other elements open the file in the browser)
          */
          this.$refs.fileform.addEventListener(evt, function(e){
            e.preventDefault();
            e.stopPropagation();
          }.bind(this), false);
        }.bind(this));

        /*
          Add an event listener for drop to the form
        */
        this.$refs.fileform.addEventListener('drop', function(e){
          /*
            Capture the files from the drop event and add them to our local files
            array.
          */
          for( let i = 0; i < e.dataTransfer.files.length; i++ ){
            this.files.push( e.dataTransfer.files[i] );
          }
        }.bind(this));
      }
    },

    methods: {
      /*
        Determines if the drag and drop functionality is in the
        window
      */
      determineDragAndDropCapable(){
        /*
          Create a test element to see if certain events
          are present that let us do drag and drop.
        */
        var div = document.createElement('div');

        /*
          Check to see if the `draggable` event is in the element
          or the `ondragstart` and `ondrop` events are in the element. If
          they are, then we have what we need for dragging and dropping files.

          We also check to see if the window has `FormData` and `FileReader` objects
          present so we can do our AJAX uploading
        */
        return ( ( 'draggable' in div )
                || ( 'ondragstart' in div && 'ondrop' in div ) )
                && 'FormData' in window
                && 'FileReader' in window;
      }
    }
  }
</script>

It just simply provides a form where we can drag and drop a file over the form. We also check to see if the form is drag and drop capable.

When the file is dragged and dropped, we add it to the local files array. All of these steps I went through up above in the tutorial. Now we will add a few things to make the drag and drop instant.

Add submitFiles() method

This method is the same method we used in the other drag and drop upload component. What this does is submits the form with the files to the server. Add the following method to the methods object:

/*
  Submits the files to the server
*/
submitFiles(){
  /*
    Initialize the form data
  */
  let formData = new FormData();

  /*
    Iteate over any file sent over appending the files
    to the form data.
  */
  for( var i = 0; i < this.files.length; i++ ){
    let file = this.files[i];

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

  /*
    Make the request to the POST /file-drag-drop-instant URL
  */
  axios.post( '/file-drag-drop-instant',
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      },
      onUploadProgress: function( progressEvent ) {
        this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
      }.bind(this)
    }
  ).then(function(){
    console.log('SUCCESS!!');
  })
  .catch(function(){
    console.log('FAILURE!!');
  });
}

What we also need to add, is add our uploadPercentage variable to our data() method and add the progress bar back to our template. Our component should now look like this:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
    border-radius: 4px;
  }

  progress{
    width: 400px;
    margin: auto;
    display: block;
    margin-top: 20px;
    margin-bottom: 20px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
      <span class="drop-files">Drop the files here!</span>

      <progress max="100" :value.prop="uploadPercentage"></progress>
    </form>
  </div>
</template>

<script>
  export default {
    /*
      Variables used by the drag and drop component
    */
    data(){
      return {
        dragAndDropCapable: false,
        files: [],
        uploadPercentage: 0
      }
    },

    mounted(){
      /*
        Determine if drag and drop functionality is capable in the browser
      */
      this.dragAndDropCapable = this.determineDragAndDropCapable();

      /*
        If drag and drop capable, then we continue to bind events to our elements.
      */
      if( this.dragAndDropCapable ){
        /*
          Listen to all of the drag events and bind an event listener to each
          for the fileform.
        */
        ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
          /*
            For each event add an event listener that prevents the default action
            (opening the file in the browser) and stop the propagation of the event (so
            no other elements open the file in the browser)
          */
          this.$refs.fileform.addEventListener(evt, function(e){
            e.preventDefault();
            e.stopPropagation();
          }.bind(this), false);
        }.bind(this));

        /*
          Add an event listener for drop to the form
        */
        this.$refs.fileform.addEventListener('drop', function(e){
          /*
            Capture the files from the drop event and add them to our local files
            array.
          */
          for( let i = 0; i < e.dataTransfer.files.length; i++ ){
            this.files.push( e.dataTransfer.files[i] );
          }
        }.bind(this));
      }
    },

    methods: {
      /*
        Determines if the drag and drop functionality is in the
        window
      */
      determineDragAndDropCapable(){
        /*
          Create a test element to see if certain events
          are present that let us do drag and drop.
        */
        var div = document.createElement('div');

        /*
          Check to see if the `draggable` event is in the element
          or the `ondragstart` and `ondrop` events are in the element. If
          they are, then we have what we need for dragging and dropping files.

          We also check to see if the window has `FormData` and `FileReader` objects
          present so we can do our AJAX uploading
        */
        return ( ( 'draggable' in div )
                || ( 'ondragstart' in div && 'ondrop' in div ) )
                && 'FormData' in window
                && 'FileReader' in window;
      },

      /*
        Submits the files to the server
      */
      submitFiles(){
        /*
          Initialize the form data
        */
        let formData = new FormData();

        /*
          Iteate over any file sent over appending the files
          to the form data.
        */
        for( var i = 0; i < this.files.length; i++ ){
          let file = this.files[i];

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

        /*
          Make the request to the POST /file-drag-drop-instant URL
        */
        axios.post( '/file-drag-drop-instant',
          formData,
          {
            headers: {
                'Content-Type': 'multipart/form-data'
            },
            onUploadProgress: function( progressEvent ) {
              this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
            }.bind(this)
          }
        ).then(function(){
          console.log('SUCCESS!!');
        })
        .catch(function(){
          console.log('FAILURE!!');
        });
      }
    }
  }
</script>

Right now we just allow the user to select files. We need to tie everything together and make it so when the user drops files on the form they get uploaded.

Tying Everything Together

The trick with this is to simply tie everything together by calling our submitFiles() method right after the user drops the files onto the form.

In our drop event handler in our mounted() lifecycle hook we simply need to add a call to the submitFiles() method.

Our handler should look like this:

/*
  Add an event listener for drop to the form
*/
this.$refs.fileform.addEventListener('drop', function(e){
  /*
    Capture the files from the drop event and add them to our local files
    array.
  */
  for( let i = 0; i < e.dataTransfer.files.length; i++ ){
    this.files.push( e.dataTransfer.files[i] );
  }

  /*
    Instantly upload files
  */
  this.submitFiles();
}.bind(this));

So now once a user drops files onto the form, we instantly submit the files to the server. That’s all there is to it! We added the progress bar to our element as well so this will show the progress as well.

Pointers

When doing drag and drop on an instant upload, there’s a few ways you could add some better UX (besides the obvious notifications of success or failure).

One could be to return the file URLs where they are on the server. This way you can remove the files right after upload or preview them outside of a FileReader().

Another thing is you could do an individual upload for each file. This could be interesting because you could do a progress bar for each file. To do this, you’d iterate over the files selected and create a request for each one and add a progress bar to the screen to show the status of each file.

Github Components

Make sure to head over to Github and check out the two components that are updated for Vue 3.

Conclusion

So this is a quick step through of drag and drop file uploading using VueJS and Axios. Of course, let me know if there are any questions or any features that you’d like to see added.

As I mentioned before, handling files in this matter is super helpful when building an API Driven Application. I’m writing a book where I’ll dive more into the processing on the API side along with some more tips and tricks from the javascript side. If you are interested, sign up to get progress updates in your mailbox: Server Side Up General List.

Happy file uploading!

Keep Reading
View the course View the Course Your Guide To Uploading Files with VueJS and Axios
Up Next → Preview File Uploads with Axios and VueJS

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Want to learn more about API Driven App Development?

We're writing a book on API Driven App Development. Be the first to know once it is ready!