DEV Community

Cover image for Build a streaming drag and drop upload section with Vue.js
tq-bit
tq-bit

Posted on • Edited on

Build a streaming drag and drop upload section with Vue.js

Fileuploads and the scope of this article

As this article's content is rather specific, please consider the following before reading ahead.

This article does show, how to:

✅ Directly deal with binary data in the browser, without the need for a dedicated input field.

✅ Put these into a format that can be streamed to a remote location with modern browser interfaces ( compatibility check at the end of the article ).

✅ Wrap the features up into a reusable Vue.js component. You can drop the resulting code into a .vue file and use it right away.

This article does not show, how to

❌ Extract the file from an - HTML tag inside a wrapping form - tag, which also includes the /post path

❌ Use a FormData object to which the file will be appended and sent to the server as a whole (even though that would also be doable)

Still on board? Then let's do this. Or jump right to the finished source code

Prerequisites

To follow along, you need to have a working version of Node.js and the Vue CLI installed on your machine, as well as a basic understanding of how Vue.js components work. The article was written using Vue 2.6.11 but it should work just as well with later versions

# Install the Vue CLI globally, in case you do not have it yet
$ npm i -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

Get started

As the topic is very specific, let's start with cloning this Github template repository to your local machine. It includes a basic structure created with the Vue CLI. The most relevant file will be AppFileupload.vue inside the components folder.

Move into a dedicated project folder and execute the following commands:

# Clone the repository
$ git clone https://github.com/tq-bit/vue-upload-component.git
$ cd vue-upload-component

# Install node dependencies and run the development server
$ npm install
$ npm run serve
Enter fullscreen mode Exit fullscreen mode

Open up your browser at http://localhost:8080 to find this template app:

an image that shows the starting template of the drag and drop application

While you could use a standard file-input html tag to receive files per drag & drop, using other tags requires a bit of additional work. Let's look at the relevant html - template snippet:

<div class="upload-body">
 {{ bodyText || 'Drop your files here' }}
</div>
Enter fullscreen mode Exit fullscreen mode

To enable the desired functionality, we can use three browser event handlers and attach them to the upload-body. Each of them is fired by the browser as seen below:

Event Fires when
dragover The left mouse button is down and hovers over the element with a file
drop A file is dropped into the designated element's zone
dragleave The mouse leaves the element zone again without triggering the drop event

Vue's built-in vue-on directive makes it simple to attach functions to these events when bound to an element. Add the following directives to the template's upload-body tag:

<div 
 v-on:dragover.prevent="handleDragOver"
 v-on:drop.prevent="handleDrop"
 v-on:dragleave.prevent="handleDragLeave"
 class="upload-body"
 >
 {{ bodyText || 'Drop your files here' }}
</div>
Enter fullscreen mode Exit fullscreen mode

Also, within the data() - method in the script - part, add these two indicators that change when above events are fired. We will use them later for binding styles and conditionally displaying the footer.

<script>
data() {
  return {
   // Create a property that holds the file information
   file: {
    name: 'MyScreenshot.jpg',
    size: 281923,
   },
   // Add the drag and drop status as an object
   status: {
    over: false, 
    dropped: false
   }
  };
},
</script>
Enter fullscreen mode Exit fullscreen mode

Next, add the following three methods below. You could fill each of them with life to trigger other UI feedback, here, we'll focus on handleDrop.

<script>
data() {...},

methods: {
 handleDragOver() {
  this.status.over = true;
 }, 
 handleDrop() {
  this.status.dropped = true;
  this.status.over = false;
 },
 handleDragLeave() {
  this.status.over = false;
 }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Before we do, let us add two more directives to our html template to conditionally show some file metadata, and style the upload-body background.

<!-- The body will serve as our actual drag and drop zone -->
<div 
 v-on:dragover.prevent="handleDragOver"
 v-on:drop.prevent="handleDrop"
 v-on:dragleave.prevent="handleDragLeave"
 class="upload-body"
 :class="{'upload-body-dragged': status.over}"
 >
 {{ bodyText || 'Drop your files here' }}
</div>

<div class="upload-footer">
 <div v-if="status.dropped">
  <!-- Display the information related to the file -->
  <p class="upload-footer-file-name">{{ file.name }}</p>
  <small class="upload-footer-file-size">Size: {{ file.size }} kb</small>
 </div>
 <button class="upload-footer-button">
  {{ footerText || 'Upload' }}
 </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Let's also add the necessary styles in the - section of the component to indicate when a file is hovering over the landing zone:

<style>
/* ... other classes*/
.upload-body-dragged {
 color: #fff;
 background-color: #b6d1ec;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Now try and throw a file inside - you'll notice the background turns blue while the footer text appears as the events are being fired.

So far so good. Let's now dive into the handleDrop method.

Catch the dropped file and process it

The instant you drop the file, it becomes available as a property of the browser event. We can then call upon one of its methods to assign it to a variable.

Add the following inside the handleDrop() method:

const fileItem = event.dataTransfer.items[0].getAsFile();
Enter fullscreen mode Exit fullscreen mode

This is what the browser's console displays the dropped item like. We do not only get access to the file itself, but also to a few useful information about it.

That's a perfect opportunity for some user feedback! Add the following to the bottom of the handleDrop() method:

this.file = {
 name: fileItem.name,
 size: (fileItem.size / 1000).toFixed(2),
};
Enter fullscreen mode Exit fullscreen mode

Finally, we can now make use of the Filereader API to catch the actual file contents and prepare it for further processing.  

Please note that this process can be customized, based on your app's needs. The following procedure makes the file available as an ArrayBuffer to process the image's raw binary data.

Add the following to the bottom of the handleDrop() - method and optionally uncomment / remove unrelevant parts:

const reader = new FileReader();

// Interchange these methods depending on your needs: 

// Read the file's content as text
// reader.readAsText(fileItem);

// Read the file's content as base64 encoded string, represented by a url
// reader.readAsDataURL(fileItem);

// Read the file's content as a raw binary data buffer
reader.readAsArrayBuffer(fileItem);

// Wait for the browser to finish reading and fire the onloaded-event:
reader.onloadend = event => {
 // Take the reader's result and use it for the next method
 const file = event.target.result;
 this.handleFileupload(file);
 // Emit an event to the parent component
 this.$emit('fileLoaded', this.file)
};
Enter fullscreen mode Exit fullscreen mode

In a nutshell, an array buffer is the most generic type our file could take. While being performant, it might not always be the best choice. You can read more on the matter at javascript.info and this article on stackabuse.

Stream the file to a server

As stated, we will not send the file as a whole, but stream it to a receiving backend. Luckily, the browser's built in fetch API has this functionality by default.

For the purpose to test our app, I've created a node.js service on heroku that interprets whatever file is POSTed and sends back a basic response. You can find its source code here: https://github.com/tq-bit/vue-upload-server.

Let's use that one in our app. Add the following code as a method to your AppFileupload.vue file:

async handleFileupload() {
 const url = 'https://vue-upload-server.herokuapp.com/';
 const options = { method: 'post', body: this.file.value };
 try {
  const response = await fetch(url, options);
  const data = await response.json();
  const { bytes, type } = data;
  alert(`Filesize: ${(bytes / 1000).toFixed(2)} kb \nType: ${type.mime}`)
 } catch (e) {
  alert('Error! \nAn error occured: \n' + e);
 }
},
Enter fullscreen mode Exit fullscreen mode

Now try to drop a file and hit 'Upload' - if it goes well, you'll receive a response in the form of an alert with some basic info about your file.

That's it. You've got a fully functioning upload component. And you're not bound to vue.js. How about trying to integrate the same functionality into a vanilla project? Or extend the existing template and add custom properties for headingText and bodyText?

To wrap this article up, you can find the finished Github repository below.

Happy coding

https://github.com/tq-bit/vue-upload-component/tree/done

Bonus: Add a svg loader

Since communication can take a moment, before wrapping up, let us add a loading indicator to our app. The svg I am using comes from loading.io, a website that, besides paid loaders, also provides free svg loaders.

In the template part of your component, replace the upload-body - div with the following:

<div
 v-on:dragover.prevent="handleDragOver"
 v-on:drop.prevent="handleDrop"
 v-on:dragleave.prevent="handleDragLeave"
 class="upload-body"
 :class="{ 'upload-body-dragged': status.over }"
>
 <svg
  v-if="loading"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  style="margin: auto; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;"
  width="160px"
  height="105px"
  viewBox="0 0 100 100"
  preserveAspectRatio="xMidYMid"
  >
   <path
    fill="none"
    stroke="#486684"
    stroke-width="8"
    stroke-dasharray="42.76482137044271 42.76482137044271"
    d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
    stroke-linecap="round"
    style="transform: scale(0.8); transform-origin: 50px 50px; animation-play-state: running; animation-delay: 0s;"
    >
    <animate
     attributeName="stroke-dashoffset"
     repeatCount="indefinite"
     dur="1s"
     keyTimes="0;1"
     values="0;256.58892822265625"
     style="animation-play-state: running; animation-delay: 0s;"
     ></animate>
  </path>
 </svg>
 <span v-else>{{ bodyText || 'Drop your files here' }}</span>
</div>
Enter fullscreen mode Exit fullscreen mode

Also, add the following on top of your data () - function:

data() {
 return {
  loading: false,
  /* ... other data props ... */ 
 };
},
Enter fullscreen mode Exit fullscreen mode

Now, when you upload a file, you should notice the loader to appear instead of the text.

This post was originally published at https://blog.q-bit.me/build-a-drag-and-drop-upload-section-with-vue-js/
Thank you for reading. If you enjoyed this article, let's stay in touch on Twitter 🐤 @qbitme

Top comments (3)

Collapse
 
roeland profile image
Roeland

I now this is an old article, but the html seems te be broken. The 2nd part is not readable.

Collapse
 
tqbit profile image
tq-bit

Hello Roe,

thanks for the headsup, a tag was causing problems during md compilation :)<br> Fixed it just now</p>

Collapse
 
roeland profile image
Roeland

Thanks. There are also some broken images, but I can read your article now. Good job!