Step by Step: Custom drag & drop upload component in Vuetify (Vue 2)
A few weeks ago I had to implement a drag and drop upload component for a project I was working on. Vuetify only provides a component that works by clicking to upload, called file input, and to be honest the user interface is not so great (you can check it here or check the image below 👇).
I started searching online to find a solution to my problem, but I had no luck. There were some good solutions (1, 2, 3) from which I got my inspiration to create this component, but there was no single solution that satisfied my requirements. Therefore, I decided to develop my own. So here we go. My first article in medium and hopefully first of many!
To avoid wasting your time, check the example of the completed component below and make sure that this is what you are looking for.
Furthermore, I assume that you have a basic understanding of how Vue and Vuetify work. If you don’t, stop here, have a look at the awesome documentation provided by Vue and Vuetify and come back to implement this component.
Creating the upload component
The first thing in our development will be to create the upload component and develop the user interface. Start by creating a new Vue component called Upload. Your code now should look like this 👇
One of the requirements for the upload component is to act as a pop up dialog box. Therefore, we are going to use the v-dialog component from Vuetify and combine it with the v-card component for a better design. From the v-card we will only use the v-card-text and v-card-actions components. If you want to use the v-card-title component too, you can. It depends on how you want to style your component.
Displaying the component
Before starting to implement any functionality for dragging & dropping files, clicking to select files or uploading, we need to be able to display our component so that we can test as we code.
The first step is to create a prop that will control when our component will be displayed. In the example below, this prop is called dialog. For displaying the upload pop up box we can pass a variable to the dialog prop of the upload component, that will be binded to the v-dialog. When this variable is true the upload component will be displayed and vice versa.
For closing the upload box all we have to do is change the value of the dialog variable to false. However, since this will be done from the dialog component, the approach above won’t work. Why?
Vue prohibits props from being mutated directly in a child component. If we bind the dialog prop to the v-dialog component by using v-model, we will get this error: “Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders”. Therefore, we have to use the sync modifier, that was added to Vue in the 2.3.0 version. If you are not familiar with the .sync modifier have a look here.
We will provide two ways to our users to close the dialog box. The first way will be by clicking outside the box and this will be done by using the @click:outside event. The second way will be by adding an “X” button on the bottom right corner of our component . To implement this, I created a method called closeDialog.
Note: Use the v-card-actions and v-spacer components to position the button at the bottom right corner
Improving the UI and UX
Right now our component is just an empty white box with two buttons at the bottom right corner. That’s not ideal. Our users won’t know how to upload their files. Let’s add some text to guide them. Feel free to change the design to suit your preferences.
Now our component looks like this 👇, which is much better.
Adding the drag and drop functionality
To implement the drag and drop functionality of our component, we will make use of the Drag and Drop API provided. If you are not familiar with the api please spend a few minutes understanding how it works and the drag events and interfaces it offers. In our case, we will only use these events:
1. @drop : To detect when the items are dropped in our drop zone area
2. @dragover: To detect when the items are dragged over our drop zone
3. @dragenter: To detect when the items enter our drop zone
4. @dragleave: To detect when the items leave our drop zone
In my case, I decided to specify the whole upload box as my drop zone area. Therefore, I implemented these events in my v-card component. However, it is possible to specify them in any other component and narrow down the area which the user can use to drop the files.
To make our component a bit more reactive, the last 3 events will be used to check if the dragged items are over our drop zone. If they are (meaning that the @dragover and @dragenter are triggered) the value of a variable will be changed to true. When the user drops the items in the drop zone (@drop triggered) or leaves the drop zone while still dragging the items (@dragleave triggered) this variable is changed back to false.
To improve the user experience, we will change the background colour of the v-card component and make the upload icon move up when the variable is true. This can easily be done by binding the class of these two components to the variable. If you are not familiar with binding classes have a look at the docs or this tutorial from VueMastery. In the example below, this variable is called dragover.
On Drop event
Now it’s time to retrieve the files uploaded by the user. As of now when the user drops the files in the drop zone, we only set the dragover variable to false. Let’s implement a onDrop method to add more functionality.
First of all, we have to pass the $event variable to our method. This event variable, includes a dataTransfer object that is used to hold the data that is being dragged during a drag and drop operation. One of the properties of this object is the files property that contains a list of all the local files in the drag operation. So the files uploaded by the user can be accessed in
Following, to make our component more generic to cover multiple use cases, we can also add a prop to control if the user can upload multiple files or not. So in our onDrop method we can check how many files the user has uploaded and if the component is supposed to accept multiple files. Also, we will declare an array to store our files. It might seem redundant to do so since we have the .files property, but we should keep in mind that a user will be able to upload files by clicking on the component too and storing both in a single array will make our code more concise and readable.
Resetting the uploadedFiles
There are two cases where we will need to reset the uploadedFiles variable. The first one is when the user closes the dialog box. In the case that we don’t remove the uploaded files when the dialog box closes, if the user opens the upload box again before reloading the web app, it will still contain the previous files that the user cancelled. So, in our closeDialog method we add
this.uploadedFiles = ;
The second case is when a user uploads some files, but then change their mind and then try to replace them by uploading some other files without removing the previous ones. In this case, we can check in our onDrop method if the uploadedFiles array is populated. If it is then we can reset it.
if (this.uploadedFiles.length > 0) this.uploadedFiles = ;
Displaying the uploaded files
Now that our user can drag and drop files into our upload component, we can improve our user experience by displaying the files that have been uploaded. We will do this by using the v-virtual-scroll and the v-list-item components. We use the scroll component to be able to display all the uploaded files without having to worry how much our dialog box we expand. (In the case that your upload component is intended to be used only for uploading less than 3 files, you can skip the v-virtual-scroll component). Check the code below 👇. An explanation is following right after so if you don’t understand what’s going on… no need to panick!
There is a lot going on in our code, so let’s break it down and explain everything. First of all, I use the v-if directive to check if there are any uploaded files. If there aren’t we simply don’t display the scroll component. Then, I customise the scroll component by setting the height to 150 and the height of each item/file to 50 so that 3 files can be viewed at the same time. Following, to customise how the items/files are displayed we have to use the default template. Inside the template, I make use of the v-list component to display the name of the file, its size in bytes and a button for removing them from the list. The removeFile method, is called whenever the user clicks our button and gets the name of the file. Since the names of files are most of the time unique, I simply search for the name of the removed file in the array, find its index and then use the splice method to remove it. If you have followed all the above steps your component should look like this 👇
Sending the uploaded files to the parent component
Finally, to keep our upload component as simple as possible, instead of processing the uploaded files here, we will pass them to the parent component. This will be done by using a custom event called files uploaded. So when our upload icon is clicked we will call a submit method and we will:
1. Check if there are any uploaded files to submit
2. Display an error notification if there aren’t and exit
3.Emit them to the parent component using the uploadedFiles event
4. Close the dialog box
This approach will allow us to keep our component as simple as possible. Any specific logic required to process the uploaded files will be added to the component who uses our upload dialog box. For example, if we want to use the upload component to upload multiple images in a Vue page, we can use:
<Upload :dialog.sync="uploadDialog" :multiple="true" @filesUploaded="processUpload($event)"
Then to process the images (e.g upload them to our database), we can define a method called processUpload in our parent component and upload them there. Then, if we decide to use the upload component again for parsing csv files in another part of our web app then we can follow the same steps. Therefore, our upload component won’t become mixed up with methods for specific functions.
Thank you for reading my article. If you have any questions or suggestions to improve this component please let me know in the comments.
Bonus: Adding the click to upload functionality
To be continued next week…