Drop and Click File Upload with VuetifyJs

Claus Straube
The Startup
Published in
7 min readApr 24, 2020
Sceenshot of the final drag & drop & click component.

VuetifyJs comes with a bunch of great Material Design components. The file upload component is like the standard HTML 5 component in Material Design style. It works, but you can’t use drag & drop. If you need a drag & drop file component and you want to use VuetifyJs (what you definitely should), this little blog post is exactly for you. After reading (and little bit of coding), you’ll end up with a shiny drag & drop file component. Yours might be even prettier than mine.

I assume that you’ve a running VueJs project with VuetifyJs, so I skip that project creation stuff. If you want to try this tutorial in a fresh setup, please read the Vuetify Quick Start tutorial. Another point: I’m using typescript (and I can recommend this). If you’re using Javascript things might be slightly different in your component, but this should be doable.

Basic design of the dropzone

Ok — fist step. We need a new component:

<template></template>
<script lang="ts">
import Vue from "vue"
import { Component } from "vue-property-decorator"
@Component
export default class FileDrop extends Vue{

}
</script>

Next we have to add a vuetify component, that represents the dropzone. I’m using for this a v-sheet component, but you can use what ever you want.

<template>   
<v-sheet
tabindex="0" [1]
title="Click to grap a file from your PC!"
color="indigo lighten-4"
width="100%"
height="200"
class="pa-2"
>
</v-sheet>
</template>

There’s nothing special about the v-sheet component. Be sure, that you provide a tabindex attribute [1]. This is not necessary for the base functionallity, but it makes the component accessible by keybord.

If you only want to drop files into the component, you’ve done so far. But in the heading I promised, that we’ll end with a drag and click dropzone. To add the click functionality, we can simply use the standard HTML 5 file input component.

<template>   
<v-sheet
tabindex="0"
title="Click to grap a file from your PC!"
color="indigo lighten-4"
width="100%"
height="200"
class="pa-2"
>
<input
type="file" [1]
accept="text/xml"
style="display:none"/> [2]
</v-sheet>
</template>

Simply place this inside the v-sheet component (or what ever component you’re using). Be sure, that it is of type “file” [1]. We don’t want to see, the component itself, but only borrow its functionallity. So we have to hide ist. This is done by display:none inside the style attribute [2].

We now have the visual base for our component. Let’s add it to the application. In a fresh demo project open the App.vue file and import our new drag & drop component.

<template>
<v-app>
...
<v-content>
<v-container fill-height> [1]
<v-row>
<v-col cols="2"></v-col>
<v-col cols="8">
<file-drop></file-drop> [2]
</v-col>
<v-col cols="2"></v-col>
</v-row>
</v-container>
</v-content>
</v-app>
</template>
<script lang="ts">
import Vue from "vue"
import { Component } from "vue-property-decorator"
// components
import FileDrop from "@/components/FileDrop.vue" [3]
@Component({
components: {
FileDrop [4]
}
})
export default class App extends Vue {
}
</script>
  1. Stuff to beautify your screen :)
  2. The file-drop component itself.
  3. Import your component.
  4. Register the component.

If you’ll send npm run serve , you can see somethin like this on your screen:

The blank dropzone.

The drag & drop functionallity

A nice indigo colored dropzone with… absolutely no functionality. Ok — let’s change this. Head back to your component file. To create a working dropzone we have to register some event listeners. So the component can “see” and react on different user interactions. The listeners should registered as early as possible. But be aware that we must have access to the HTML components to register any listener. So mounted should be the perfect lifecycle phase for this.

<script lang="ts">
...
@Component
export default class FileDrop extends Vue{

mounted () {
const dropzone = this.$el [1]
const fileupload = this.$el.firstElementChild as HTMLElement [2]
}
}
</script>

The dropzone itself (the v-sheet component) can be accessed by this.$el [1], the fileupload is simply the first element child [2]. Be aware, that this is also true after beautifying your component ;)

First of all, let’s implement the drop functionality. If we want to change a icon, color of the dropzone, or what ever, we need a status property [1]. The dragenter , dragover and dragleave events [2] will change this status on every move. If you don’t need this — leave the first three event listeners off. The last event listener [3] is crucial for the file drop functionality. So we need this one in every case.

@Component
export default class FileDrop extends Vue{

// internal properties
dragover: boolean = false [1]

mounted () {
const dropzone = this.$el
const fileupload = this.$el.firstElementChild as HTMLElement
// register listeners on your dropzone / v-sheet
if(dropzone) {
// register all drag & drop event listeners
dropzone.addEventListener("dragenter", e => { [2]
e.preventDefault()
this.dragover = true
})
dropzone.addEventListener("dragleave", e => { [2]
e.preventDefault()
this.dragover = false
})
dropzone.addEventListener("dragover", e => { [2]
e.preventDefault()
this.dragover = true
})
dropzone.addEventListener("drop", e => { [3]
e.preventDefault()
const dragevent = e as DragEvent
if(dragevent.dataTransfer) {
// do something
}
})
}
}
}

What should be done if the file has been dropped to the dropzone? Keeping in mind, that the drop & click component should be reusable, we simply send an event (with all files in it) to the parent component.

@Component
export default class FileDrop extends Vue{

// internal properties
dragover: boolean = false

mounted () {
const dropzone = this.$el // register listeners on your dropzone / v-sheet
if(dropzone) {
...
dropzone.addEventListener("drop", e => {
e.preventDefault()
const dragevent = e as DragEvent
if(dragevent.dataTransfer) { [1]
this.filesSelected(dragevent.dataTransfer.files)
}
})
}
}
@Emit()
filesSelected(fileList: FileList) { [2]
this.dragover = false
}
}

Of course this event [2] should contain the dropped files. It will be fired, if the drop event has a filled dataTransfer property [1]. The parent component can reach the drop event like this:

<file-drop v-on:files-selected="doSomething"></file-drop>

Ok. Drag & drop functionally — check!

The click and key press functionallity

If you want add the click and key press (focus by tab and press enter) behavior, you should read on ;)

Most of the work has already been done. Adding the additional functionality is as easy, as putting three event listeners to your code:

@Component
export default class FileDrop extends Vue{

// internal properties
formUpload: boolean = false
dragover: boolean = false
mounted () {
// to register listeners, we have to know the
// html elements
const dropzone = this.$el
const fileupload = this.$el.firstElementChild as HTMLElement
// register listeners on your dropzone / v-sheet
if(dropzone) {
...
dropzone.addEventListener("click", e => { [1]
e.preventDefault()
if(fileupload) {
fileupload.click()
}
})
dropzone.addEventListener("keypress", e => { [2]
e.preventDefault()
const keyEvent = e as KeyboardEvent
if (keyEvent.key === "Enter") {
if(fileupload)
fileupload.click()
}
})
// register listeners on the file input
if(fileupload) {
fileupload.addEventListener("change", e => { [3]
const target = (e.target as HTMLInputElement)
if(target.files) {
this.filesSelected(target.files)
}
})
}
}
}
@Emit()
filesSelected(fileList: FileList) {
this.dragover = false
}
}

The first one [1] is used to register click events on our dropzone. If someone clicks the dropzone, a virtual click will be triggered on the hidden input element, which opens the (browser) file select dialog. The second one [2] does the same as the click event listener, but on key press event. More precise — on pressing the enter key.

Finally we need an event listener on the file input component [3], that fires, if the user selects files over the browser dialog. This one checks, if files have been selected and if yes, filesSelected event will be triggered (the same as for drop events).

Ok. Press and click functionality — check!

A more beautiful dropzone

If you more purist — skip this section. A single colored dropzone is exact the perfect thing for you :)

<template>   
<v-sheet
id="dropzone"
ref="dzone"
tabindex="0"
title="Click to grap a file from your PC!"
color="indigo lighten-4"
width="100%"
style="cursor:pointer;"
height="200"
class="pa-2"
>
<input
ref="upload"
id="fileUpload"
type="file"
accept="text/xml"
style="display:none"/>
<v-row justify="center">
<v-icon
v-if="!dragover"
color="indigo darken-2"
size="75"
>mdi-cloud-upload-outline</v-icon>
<v-icon
v-if="dragover"
color="indigo darken-2"
size="75"
>mdi-book-plus</v-icon>
</v-row>
<v-row justify="center">
<span class="title indigo--text text--darken-2">Drag'n drop or click to upload file!</span>
</v-row>
</v-sheet>
</template>

You can put into the dropzone what ever you want. But be aware, that the hidden input is always the first element inside the v-sheet component. Otherwise this

const fileupload = this.$el.firstElementChild as HTMLElement

won’t work anymore. Your final result should look like this (or more beautiful).

The final result :)

You can find the sources on a github repository. I hope you have enjoyed this article. Please leave a comment if you have any question or would like to leave feedback.

--

--