はじめに
最近ファイルアップロード画面を作った時に、ドラッグアンドドロップでファイルを取得するコンポーネントを作りました。この時得た知見をまとめてみたいと思います。
開発環境
- VueCLI
- Vue 2.x
- TypeScript
- vue-property-decorator
シンプルな実装
<template>
<div
class="file-upload-card"
@dragover.prevent="drag = true"
@dragleave.prevent="drag = false"
@drop.prevent="onDrop"
>
<div v-if="!drag">
ドラッグアンドドロップでファイルを追加
</div>
<div v-else>
ドラッグ中
</div>
<div v-if="file">
ファイル名: {{ file.name }}
<button @click="file = null">
クリア
</button>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator"
@Component
export default class FileUploadCard extends Vue {
drag = false
file: File | null = null
onDrop(event: DragEvent): void {
this.drag = false
if (!event) {
return
}
if (!event.dataTransfer) {
return
}
if (event.dataTransfer.files.length === 0) {
return
}
this.file = event.dataTransfer.files[0]
}
}
</script>
<style lang="scss" scoped>
.file-upload-card {
display: flex;
flex-direction: column;
border: solid 1px;
padding: 1rem;
width: 20rem;
}
button {
color: white;
background: gray;
padding: 0.2rem;
}
</style>
ポイント
ドラッグ状態の取得
dragover
は要素上でドラッグ操作をしているとき、dragleave
は要素上からドラッグしているカーソルが離れたときに発行されるイベントです。これらのイベントで drag
の true/false を切り替えることでドラッグ状態を取得できます。
ドロップ時の処理
drop
がドロップイベントです。この時 dragleave
は発行されないので、onDrop
でも drag
を false にする処理を入れてあります。
必ず prevent を付ける
@dragover.prevent
のように .prevent
修飾子を付けていますがこれはこのイベント時に行われるブラウザのデフォルトの動作を無効化するものです。
この修飾子を付けなかった場合、ブラウザ上でそのファイルが展開されてしまいます。
イベントの型は DragEvent
TypeScript を使っていると悩みがちなイベント型ですが、ドラッグ時のイベントは DragEvent
を指定しておくといい感じにファイル取得の処理を書くことができます。
抽象化してみる
あなたのプロジェクトで、ドラッグアンドドロップ機能を持たせたいコンポーネントは一つとは限りません。複数ある場合に、毎回 @dragover.prevent="drag = true"
や、ファイルをイベントから取り出す処理を書くのは面倒です。先ほどの FileUploadCard
も下記のように書けると便利そうです。
<template>
<FileDropArea
class="file-upload-card"
:drag.sync="drag"
@drop="file = $event"
>
<div v-if="!drag">
ドラッグアンドドロップでファイルを追加
</div>
<div v-else>
ドラッグ中
</div>
<div v-if="file">
ファイル名: {{ file.name }}
<button @click="file = null">
クリア
</button>
</div>
</FileDropArea>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator"
import FileDropArea from "./FileDropArea.vue"
@Component({
components: {
FileDropArea,
},
})
export default class FileUploadCard extends Vue {
drag = false
file: File | null = null
}
</script>
// スタイルは省略
<template>
<div
@dragover.prevent="drag = true"
@dragleave.prevent="drag = false"
@drop.prevent="onDrop"
>
<slot />
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator"
@Component
export default class FileDropArea extends Vue {
drag = false
@Watch("drag")
syncOnDragChanged(): void {
this.$emit("update:drag", this.drag)
}
onDrop(event: DragEvent): void {
this.drag = false
if (!event) {
return
}
if (!event.dataTransfer) {
return
}
if (event.dataTransfer.files.length === 0) {
return
}
this.$emit("drop", event.dataTransfer.files[0])
}
}
</script>
ポイント
FileDropArea
ドラッグ/ドロップイベントの取得とファイルのオブジェクトの取り出しだけを行う抽象的な FileDropArea
を定義します。
このコンポーネントは slot
を持っているため、
<template>
<FileDropArea
class="file-upload-card"
:drag.sync="drag"
@drop="file = $event"
>
<!-- ファイルのドロップでの取得機能を持たせたい要素 -->
</FileDropArea>
</template>
このように任意の要素を挟むだけでファイルのドロップでの取得機能を持たせることができる。
:drag.sync でドラッグ状態を取得する
Vue.js では v-model 以外にも .sync 修飾子を使った双方向バインディングができます。
このように
@Watch("drag")
syncOnDragChanged(): void {
this.$emit("update:drag", this.drag)
}
子コンポーネント側で update:[event]
というイベント名で値を emit
すると、
class="file-upload-card"
:drag.sync="drag"
@drop="file = $event"
>
親コンポーネント側で :[event].sync="value"
という形式でその値を同期することができます。
ちなみに、.sync
を使った書き方は糖衣構文なので、下記と等価です。
<FileDropArea
class="file-upload-card"
@update:drag="drag = $event"
@drop="file = $event"
>
この方法は、FileDropArea
が余計なイベントを定義しなくて良いという点、drag
が変更された時だけイベントが発行され親要素に伝わるという点で優れていると思います。
drop イベントでファイルオブジェクトを emit
FileDropArea
側で
onDrop(event: DragEvent): void {
this.drag = false
if (!event) {
return
}
if (!event.dataTransfer) {
return
}
if (event.dataTransfer.files.length === 0) {
return
}
this.$emit("drop", event.dataTransfer.files[0])
}
この処理をしているため、親コンポーネント側では
@drop="file = $event"
この1行のみでファイルを取得することが可能となります。
おわりに
.prevent
やイベントの型は知らないとハマりがちだと思います。同じ轍を踏まない人が少しでも増えると幸いです。
今回実際に使ったソースコードは以下です。
https://github.com/punkshiraishi/file-drop-sample