vue.jsを使ってアプリケーションを構築する際にドラッグ&ドロップでファイルのアップロードを行いたい時、ライブラリなしでコードを書きたいと思ったことはありませんか?本文書はそんな人のためにvue.js入門者でもできるドラッグ&ドロップのファイルアップロードについて基礎から説明を行っていきます。今までライブラリに頼って一度もドラッグ&ドロップの自作にチャレンジしていない人であればこんなに簡単なの!と驚くこと間違いなしです。

初期設定

手元の環境ですぐに動作確認ができるようにvue.jsはcdnを利用します。任意のフォルダを作成し、index.htmlファイルを作成してください。

index.htmlファイルに以下のコードを記述しファイルをドロップする四角枠を作成します。


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue.js Drag&Drop File Upload</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<style>
    html,
    body {
        height: 100%;
    }

    body {
        display: flex;
        justify-content: center;
        align-items: center;
    }

    .drop_area {
        color: gray;
        font-weight: bold;
        font-size: 1.2em;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 500px;
        height: 300px;
        border: 5px solid gray;
        border-radius: 15px;
    }
</style>

<body>
    <div id="app">
        <div class="drop_area">
            ファイルアップロード
        </div>
    </div>

    <script>
        const app = new Vue({
            el: "#app",
            data: {

            }
        })
    </script>

</body>

</html>

比較的長いCSSの設定になっていますが中身は単純でFlexboxを利用して画面の中央にファイルをドロップするためのターゲットとなる500px☓300pxの四角枠を作成しているだけです。

画面中央にドロップエリアを作成
画面中央にドロップエリアを作成

dragenter, dragleaveイベントの動作確認

ドラッグ&ドロップを使ったアプリケーションを構築するためにはHTML5が持つdrag&dropに関するイベント利用します。本文書では4つのイベントを利用するので一つ一つどのようなイベントなのか説明を行っていきます。

dragenterイベントの動作確認

ファイルアップロード機能を持っている一般的なアプリケーションではドロップエリアまたはブラウザに向けてファイルをドラッグすると画像の色が変わるものをよく見かけます。同じようにvue.jsのdragenterイベントとclassバインドを利用して中央の四角い枠の色を動的に変更します。

ドロップエリアのdivタグにdragenterイベントを追加しdragEnterメソッドを設定します。


<div class="drop_area" @dragenter="dragEnter">
    ファイルアップロード
</div>

vue.jsにdragEnterメソッドを追加します。動作確認なのでdragenterイベントが発生したらコンソールにEnter Drop Areaの文字列を表示させます。


methods: {
    dragEnter() {
        console.log('Enter Drop Area');
    }
}

ブラウザ上のドロップエリアにファイルをドラッグしてください。Chromeであればデベロッパーツールのコンソールに”Enter Drop Area”が表示されれば正常に設定が行われています。

dragenterイベントでコンソールにメッセージを表示
dragenterイベントでコンソールにメッセージを表示

classバインドによる動的な適用

classバインドを行うためにデータプロパティisEnterを追加します。isEnterはデフォルトではfalseに設定し、dragEnterイベントが発生するとtrueに変更します。


const app = new Vue({
    el: "#app",
    data: {
        isEnter: false
    },
    methods: {
        dragEnter() {
            this.isEnter = true;
        }
    }
})

ドロップエリアのdivにclassバインドを追加します。下記の設定ではisEnterの値がtrueの場合にクラスのenterが適用されます。falseの場合にはclassのenterは適用されません。isEnterの値によりclassの適用、非適用を切り替えます。


<div class="drop_area" 
     @dragenter="dragEnter" 
     :class="{enter: isEnter}">
    ファイルアップロード
</div>

styleタグの中にクラスのenterを追加します。ボーダーの色と太さと形状を設定しています。


.enter {
    border: 10px dotted powderblue;
}

ブラウザ上のドロップエリアにファイルをドラッグしてください。四角い枠に入った瞬間にenterクラスが適用されドロップエリアの枠が変わります。

枠の色、形状が動的に変わる
枠の色、形状が動的に変わる

一度変わってしまうと解除する設定を行っていないのでブラウザをリロードしない限りはドットの枠線がついたままになります。

ドロップエリアに子要素がある場合は子要素の上を通るとdragleaveのイベントが発生します。その場合は子要素にCSSのpointer-events: none;を設定してください。
fukidashi

Dragleaveイベントの動作確認

dragenterではドロップエリアに入った場合、次はドロップエリアの外に出た時に発生するdragleaveイベントを設定します。


<div class="drop_area" 
     @dragenter="dragEnter" 
     @dragleave="dragLeave" 
     :class="{enter: isEnter}">
    ファイルアップロード
</div>

dragleaveイベントに指定したdragLeaveメソッドをvue.jsに追加します。dragEnterとは逆でデータプロパティisEnterの値をfalseに設定します。


const app = new Vue({
    el: "#app",
    data: {
        isEnter: false
    },
    methods: {
        dragEnter() {
            this.isEnter = true;
        },
        dragLeave() {
            this.isEnter = false;
        },
    }
})

dragenterとdragleaveが設定できればファイルをドロップエリアにドラッグすればボーダーがドットの枠線になり、ドロップエリアの外に出ると枠線が実線の元の線に戻ります。

Dragoverイベントについて

DropEnterとDropLeaveの他にドロップエリアの上にファイルをドラッグした場合に発火されるDragoverイベントがあります。

ドロップエリアのdivタグにdragoverイベントを追加してdragOverメソッドを設定します。


<div class="drop_area" 
     @dragenter="dragEnter" 
     @dragleave="dragLeave" 
     @dragover="dragOver" 
     :class="{enter: isEnter}">
    ファイルアップロード
</div>

dragOverメソッドではイベントが発生するとコンソールにDragOverという文字列を表示させます。


dragOver() {
    console.log('DragOver')
},

ファイルをドロップエリアの上に持っていくとエリアの上にいる間ずっとイベントが発生します。

左の110がイベントが発生した回数です。

ドロップエリアの上にファイルをドラッグするとイベント発生
ドロップエリアの上にファイルをドラッグするとイベント発生

Dropイベントの動作確認

最後に説明するDropイベントを利用するとドロップエリアにドロップしたファイルの情報を取得することができます。

ドロップエリアのdivにdropイベントを追加し、dropFileメソッドを設定します。


<div class="drop_area" 
        @dragenter="dragEnter" 
        @dragleave="dragLeave" 
        @dragover="dragOver"
        @drop="dropFile" 
        :class="{enter: isEnter}">
        ファイルアップロード
    </div>

dropFileメソッドではイベントが発生するとコンソールにDropped Fileの文字列が表示されます。


dropFile() {
    console.log('Dropped File')
}

dragenter, dragleave, dragover, dropの4つのイベントが設定を行いました。しかしファイルをドロップエリアにドロップしてもコンソールにDropped Fileの文字列が表示されるわけではなくドロップした画像がそのままブラウザに表示されます。

ドロップしたファイルが表示される
ドロップしたファイルが表示される

preventDefaultの設定

ブラウザにファイルをドロップするとドロップしたファイルの内容が画面に表示されるというのはデフォルトの動きです。このデフォルトの動作を停止するためにpreventDefaultという設定が準備されており、vue.jsではイベントにdrop.preventのように.preventを設定を行うことでpreventDefaultを利用することができます。

drop.preventの代わりにメソッドの中でevent.preventdefault()と設定することもできます。
fukidashi

ファイルをドロップするためにはdropoverとdropにpreventを設定する必要があります。

dropoverのイベントでなにか処理を行うことはないのでdropover.preventは残りますがvue.jsに追加したdropOverメソッドは削除します。
fukidashi

<div class="drop_area" 
        @dragenter="dragEnter" 
        @dragleave="dragLeave" 
        @dragover.prevent 
        @drop.prevent="dropFile" 
        :class="{enter: isEnter}">
    ファイルアップロード
</div>

preventを設定した後にファイルをドロップエリアにドロップするとコンソールにDropped Fileが表示されます。

ファイルをドロップするとDropped Fileが表示される
ファイルをドロップするとDropped Fileが表示される

Dropped Fileが表示された後ブラウザを見るとドットの枠線のままの状態になっています。

枠の色、形状が動的に変わる
枠の色、形状が動的に変わる

それはファイルをDropしたため、ファイルが枠外に出ていないのでdragleaveイベントが発生していないためです。ファイルをDropした後に枠線が実線に戻るようにDropFileメソッドでisEnterをfalseにしておきます。これでファイルをドロップするとドット線が実線に変わります。


dropFile() {
    console.log('Dropped File')
    this.isEnter = false;
}

ファイル情報を取得する

イベントからファイル情報を取得

ファイルをドロップエリアにドロップできることが確認できたので次はドロップしたファイル情報を取得します。

取得にはイベントを利用しevent.dataTransfer.filesでファイル情報を取得することができます。

dropFileメソッドの中でファイル情報を取得してみましょう。


dropFile() {
    console.log(event.dataTransfer.files)
    this.isEnter = false;
}

ファイルをドロップエリアにドロップしてコンソールを確認するとFileListオブジェクトにファイルの情報が入っていることが確認できます。

ファイル名はlog.pngで、サイズが3451であることやファイルのtypeがpngであることもわかります。

ファイル情報を取得
ファイル情報を取得

ファイル情報をvue.jsで管理

取得したファイル情報をvue.jsで管理するためにデータプロパティのfilesを追加します。


data: {
    isEnter: false,
    files: []
},

dropFileメソッドで取得したファイル情報をfilesに保存します。保存する時はスプレッド構文を利用して保存します。


dropFile() {
    this.files = [...event.dataTransfer.files]
    this.isEnter = false;
}

これでドロップしたファイルはfilesに保存されるためvue.js上で扱うことができます。

v-forでファイル情報を表示

vue.jsのデータプロパティfilesに保存されたファイル情報をv-forを利用して展開しブラウザ上に表示します。


<div id="app">
    <div class="drop_area" @dragenter="dragEnter" @dragleave="dragLeave" @dragover.prevent @drop.prevent="dropFile"
        :class="{enter: isEnter}">
        ファイルアップロード
    </div>
    <div>
        <ul>
            <li v-for="file in files">{{ file.name }}
            </li>
        </ul>
    </div>
</div>

複数のファイルを同時にドロップエリアにドロップしてみましょう。

ドロップしたファイルの名前がドロップエリアの下に表示されます。

ファイル名がドロップエリアの下に表示される
ファイル名がドロップエリアの下に表示される

バックエンドへのファイルの送信

ブラウザ上からファイルの情報を取得することができたので、取得したファイルをバックエンドに送信することでサーバ上にファイルを保存することができます。

本文書ではバックエンドサーバ側の処理やバックエンドサーバから戻される情報の処理については記述していません。
fukidashi

formDataでファイルを送信

ファイルをバックエンドサーバに送信する際はformDataを利用します。本動作確認ではバックエンドサーバを準備していませんが、axiosを利用すると下記のコードでファイル情報をサーバに送信することができます。


dropFile() {
    this.files = [...event.dataTransfer.files]
    this.files.forEach(file => {
        let form = new FormData()
        form.append('file', file)
        axios.post('url', form).then(response => {
            console.log(response.data)
        }).catch(error => {
            console.log(error)
        })
    })
    this.isEnter = false;
}
urlにバックエンドサーバのエンドポイントを設定してください。
fukidashi

アップロードするファイル選択

ここからはvue.jsに関連するフロントエンド側の処理についてもう少し深く説明を行っていきます。

ファイルアイコン画像を設定

見栄えを変えるためにファイル名だけではなくファイル名とファイルのアイコン画像を一緒に表示させます。

index.htmlファイルが保存されているフォルダにimgフォルダを作成しファイル画像icon-file.pngを保存します。v-forの展開を行うliタグの中で画像を設定します。


<div>
    <ul>
        <li v-for="file in files">
            <img src="img/icon-file.png">{{ file.name }}
        </li>
    </ul>
</div>

ulタグのmarginやpaddingを0に設定します。


ul {
    margin: 0;
    padding: 0;
    list-style-type: none;
}

複数のファイルをドロップエリアにドロップすると下記のように表示されます。

ファイル画像が表示
ファイル画像が表示

Flexboxを利用してファイルを横並びに調整します。

各タグにclassを追加します。


<ul class="flex">
    <li v-for="file in files" class="flex-col">
        <img class="file_icon" src="img/icon-file.png">
        <span>{{ file.name }}</span>
    </li>
</ul>

追加したclassをstyleに追加します。


.flex {
    display: flex;
    align-items: center;
}

.flex-col {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 0.5em;
    font-size: 10px;
}

ブラウザでファイルをドロップエリアにドロップすると下記のように表示されます。

CSSで調整後のファイル情報
CSSで調整後のファイル情報

アップロードするファイル選択

ファイルをドロップエリアにドロップした後にアップロードするファイルを選択できるように削除ボタンを追加します。

既存のimgタグの外側にdivタグを追加し、削除ボタンはspanタグの中に&timesで記述します。

削除ボタンはdivタグを基準にpositionのabsoluteを利用してファイル画像からの位置を調整します。


<ul class="flex">
    <li v-for="file in files" class="flex-col">
        <div style="position: relative;">
            <span class="delete-mark">×</span>
            <img class="file_icon" src="img/icon-file.png">
        </div>
        <span>{{ file.name }}</span>
    </li>
</ul>

新たなclassをstyleに追加し、imgタグの外側に追加したdivにはそのままstyleでpositionのrelativeを設定しています。


.delete-mark {
    position: absolute;
    top: -14px;
    right: -10px;
    font-size: 20px;
}

ブラウザで確認するとファイル画像の右上に削除Xが表示されます。

削除マークの追加
削除マークの追加

Xをクリックするとアップロードするファイル一覧から削除できるようにクリックイベントを追加します。clickイベントにはdeleteFileメソッドを設定し引数にindexを渡します。


 <li class="flex-col" v-for="(file,index) in files" :key="index" @click="deleteFile(index)">

vue.jsのmethodsに新たにdeleteFileメソッドを追加します。受け取ったindexを使ってファイル情報を保存しているfilesからクリックしたファイルを削除しています。


deleteFile(index) {
    this.files.splice(index, 1)
}
spliceを使ってthis.filesの配列のindex番目の要素を1つ削除しています。
fukidashi

コード追加後ファイルをドロップし削除マークをクリックするとそのファイルが一覧から削除されていることを確認してください。

この設定によりドロップ後にファイルを選択する機能を追加することができました。

アップロードするファイル追加

ドロップしたファイルを削除する機能は追加できましたが一度ドロップした後に別のファイルをドロップした場合の動作を確認してみましょう。

2つファイルをドロップします。

2つのファイルをドロップ
2つのファイルをドロップ

もう一つファイルを別でドロップします。現在のコードではファイルが追加されるのではなく上書きされてしまいます。

ファイルを追加でドロップ
ファイルを追加でドロップ

追加できるようにコードの書き換えを行います。


dropFile() {
    this.files.push(...event.dataTransfer.files)
    this.isEnter = false;
},

コードを書き換えたことでファイルの追加も行うことができるようになります。

ファイル選択後にアップロード

ファイルの削除、追加ができるようになったのでブラウザ上でアップロードするファイルを決めてから送信できるように送信ボタンを追加します。

送信ボタンはファイルがドロップされた時のみ表示させるようにv-showディレクティブを利用します。v-showの表示・非表示はfiles.lengthで配列の長さで判断します。ファイルがない場合はfalseとなり、ボタンは表示されません。


<ul class="flex">
    <li class="flex-col" v-for="(file,index) in files" :key="index" @click="deleteFile(index)">
        <div style="position: relative;">
            <span class="delete-mark">×</span>
            <img class="file_icon" src="img/icon-file.png">
        </div>
        <span>{{ file.name }}</span>
    </li>
</ul>
<div v-show="files.length">
    <button class="button">送信</button>
</div>

buttonのクラスbuttonもstyleに追加します。


.button {
    padding: 0.5em 1.5em;
    background-color: #0070a7;
    color: white;
    font-size: 14px;
    font-weight: bold;
    border-radius: 5px;
    border-color: #0070a7;
}
送信ボタンはファイルをドロップしたら表示
送信ボタンはファイルをドロップしたら表示

buttonにはclickイベントを追加し、sendFileメソッドを設定します。


<div v-show="files.length">
    <button class="button" @clikc="sendFile">送信</button>
</div>

sendFileメソッドの中身は先程バックエンドへのファイルの送信で作成したコードです。


sendFile() {
    this.files.forEach(file => {
        let form = new FormData()
        form.append('file', file)
        axios.post('url', form).then(response => {
            console.log(response.data)
        }).catch(error => {
            console.log(error)
        })
    })
}

vue.jsを利用するとライブラリの力を借りなくてもドラッグ&ドロップの機能を実装することができます。