23
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ユニークビジョン株式会社Advent Calendar 2020

Day 16

【Vue.js】ドラッグアンドドロップでファイルを取得するコンポーネントを作る

Last updated at Posted at 2020-12-15

はじめに

最近ファイルアップロード画面を作った時に、ドラッグアンドドロップでファイルを取得するコンポーネントを作りました。この時得た知見をまとめてみたいと思います。

開発環境

  • VueCLI
  • Vue 2.x
  • TypeScript
  • vue-property-decorator

シンプルな実装

file-upload-1.gif

FileUploadCard.vue
<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 も下記のように書けると便利そうです。

FileUploadCard.vue
<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>
// スタイルは省略
FileDropArea.vue
<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 修飾子を使った双方向バインディングができます。
このように

FileDropArea.vue
  @Watch("drag")
  syncOnDragChanged(): void {
    this.$emit("update:drag", this.drag)
  }

子コンポーネント側で update:[event] というイベント名で値を emit すると、

FileUploadCard.vue
    class="file-upload-card"
    :drag.sync="drag"
    @drop="file = $event"
  >

親コンポーネント側で :[event].sync="value" という形式でその値を同期することができます。
ちなみに、.sync を使った書き方は糖衣構文なので、下記と等価です。

FileUploadCard.vue
  <FileDropArea
    class="file-upload-card"
    @update:drag="drag = $event"
    @drop="file = $event"
  >

この方法は、FileDropArea余計なイベントを定義しなくて良いという点、drag が変更された時だけイベントが発行され親要素に伝わるという点で優れていると思います。

drop イベントでファイルオブジェクトを emit

FileDropArea 側で

FileDropArea.vue
  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])
  }

この処理をしているため、親コンポーネント側では

FileUploadCard.vue
    @drop="file = $event"

この1行のみでファイルを取得することが可能となります。

おわりに

.prevent やイベントの型は知らないとハマりがちだと思います。同じ轍を踏まない人が少しでも増えると幸いです。

今回実際に使ったソースコードは以下です。
https://github.com/punkshiraishi/file-drop-sample

23
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?