Drop and Click File Upload with VuetifyJs
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>
- Stuff to beautify your screen :)
- The
file-drop
component itself. - Import your component.
- Register the component.
If you’ll send npm run serve
, you can see somethin like this on your screen:
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 = falsemounted () {
// 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).
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.