Build a streaming drag and drop upload section with Vue.js
Tags: javascript, snippets
Published: 2021-02-15 17:12:12

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 _<input type="file"/>_ - HTML tag inside a wrapping form - tag, which also includes the /post path

❌ Use a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/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](https://github.com/tq-bit/vue-upload-component)

Prerequisites
-------------

To follow along, you need to have a working version of [Node.js](https://nodejs.org/en/download/) and the [Vue CLI](https://cli.vuejs.org/guide/installation.html) installed on your machine, as well as a basic understanding of how [Vue.js components](https://vuejs.org/v2/guide/components.html) work. The article was written using Vue 2.6.11 but it should work just as well with later versions

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

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](https://cli.vuejs.org/). The most relevant file will be `AppFileupload.vue` inside the components folder.

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

```sh
# 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
```

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](https://q-bit.me/content/images/2021/01/01_vue_drag_and_drop.jpg)

While you could use a standard [file-input html](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file) 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:

```html
<div class="upload-body">
 {{ bodyText || 'Drop your files here' }}
</div>
```

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:

```html
<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>
```

Also, within the data() - method in the <script> - part, add these two indicators that change when above events are fired. We'll use them later for [binding styles](https://vuejs.org/v2/guide/class-and-style.html#Binding-HTML-Classes) and conditionally display our the footer.  

```javascript
<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>
```

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`.

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

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

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.

```html
<!-- 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>
```

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

```css
<style>
/* ... other classes*/
.upload-body-dragged {
 color: #fff;
 background-color: #b6d1ec;
}
</style>
```

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.

![](https://q-bit.me/content/images/2021/01/02_Vue_upload_section_hover.jpg)

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:

```javascript
const fileItem = event.dataTransfer.items[0].getAsFile();
```

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.

![](https://q-bit.me/content/images/2021/01/04_vue_screenshot_console.jpg)

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

```javascript
this.file = {
 name: fileItem.name,
 size: (fileItem.size / 1000).toFixed(2),
};
```

Finally, we can now make use of the [Filereader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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:

```javascript
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)
};
```

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](https://javascript.info/arraybuffer-binary-arrays ) and [this article on stackabuse](https://stackabuse.com/encoding-and-decoding-base64-strings-in-node-js/).

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](https://vue-upload-server.herokuapp.com/) 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](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:

```javascript
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);
 }
},
```

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.

![](https://q-bit.me/content/images/2021/01/06_vue_upload_alert.jpg)

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](https://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:

```javascript
<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>
```

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

```javascript
data() {
 return {
  loading: false,
  /* ... other data props ... */ 
 };
},
```

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](https://twitter.com/qbitme)