Angular でファイルをドラッグ & ドロップで選択させる UI を実現するディレクティブ

Angular でファイルをドラッグ & ドロップで選択させる UI を実現するためのディレクティブを作った。

世間には ngx-uploader や ng2-file-drop、ng2-file-upload など、似たようなライブラリはあるのだが、いずれも少々大仰で、ファイルアップロード機能込みで提供されていたりして鬱陶しかったので、最も基本的な部分だけを抜き出して自作することにした。

目次

ファイルをドラッグ & ドロップで取得するためのディレクティブ

ファイルのドラッグ & ドロップは、drop イベントで取得できる。

その手前に dragover というイベントがあり、ココで event.preventDefault() を実行しておかないと、ファイルをドロップした時にブラウザがそのファイルを開く挙動をしてしまった。

単にコンポーネント内で作るなら、

<div (dragover)="onDragOver($event)" (drop)="onDrop($event)"></div>

のように書けばよかったのだが、ディレクティブとして切り出してイベントを伝えるには @HostListener() というモノを使って、何やら厄介な記法を使ってやる必要があった。

以下がそのディレクティブ。

import { Directive, EventEmitter, HostListener, Output } from '@angular/core';

/**
 * ファイルをドラッグ & ドロップで取得するためのディレクティブ
 */
@Directive({
  selector: '[appFileDrop]'
})
export class FileDropDirective {
  /**
   * ファイルドロップ時のイベント
   */
  @Output()
  public onFileDrop: EventEmitter<File[]> = new EventEmitter<File[]>();
  
  /**
   * ファイルが要素にドラッグされて重なった時のイベント
   * ドラッグイベントを解除しておかないとドロップイベント時にブラウザがファイルを開く動作をしてしまう
   * stopPropagation() は不要な様子
   * 
   * @param event イベント
   */
  @HostListener('dragover', ['$event'])
  public onDragOver(event: any): void {
    event.preventDefault();
  }
  
  /**
   * ファイルドロップ時のイベント
   * 取得したファイルを引数に onFileDrop イベントを発火させる
   * 
   * @param event イベント
   */
  @HostListener('drop', ['$event'])
  public onDrop(event: any): void {
    event.preventDefault();
    this.onFileDrop.emit(event.dataTransfer.files);
  }
}

FileDropDirective の使い方

この FileDropDirective は、以下のようにして使う。

<div appFileDrop (onFileDrop)="onFileDrop($event)" class="drop-zone">Drop Here</div>

コンポーネントに定義した onFileDrop() は、$eventfiles になっているので、それを取り出して処理したりすれば良い。

@Component({ /* 省略 */ })
export class MyComponent {
  onFileDrop(files: File[]): void {
    const file = files[0];
    // ファイルを処理する…
  }
}

ファイルのドロップ領域のスタイリングは以下のような塩梅で。

.drop-zone {
  width: 300px;
  height: 300px;
  border: 5px dashed #999;
  color: #999;
  font-size: 150%;
  font-weight: bold;
  text-align: center;
  line-height: 300px;
  white-space: nowrap;
  overflow: hidden;
}

input[type="file"] でファイルを取得するディレクティブ

ドラッグ & ドロップ領域だけだと、ファイル選択ダイアログを表示させてファイル選択ができないので、通常の input[type="file"] な要素も一緒に提供しておき、どちらか好きな方でファイル選択をさせれば良いだろう。

以下が input[type="file"] な要素向けのディレクティブだ。

import { Directive, EventEmitter, HostListener, Output } from '@angular/core';

/**
 * input[type="file"] 要素でファイルを取得するためのディレクティブ
 */
@Directive({
  selector: '[appFileSelect]'
})
export class FileSelectDirective {
  /**
   * ファイル選択時のイベント
   */
  @Output()
  public onFileSelect: EventEmitter<File[]> = new EventEmitter<File[]>();
  
  /**
   * ファイル選択時のイベント
   * 取得したファイルを引数に onFileSelect イベントを発火させる
   * 
   * @param event イベント
   */
  @HostListener('change')
  public onChange(): any {
    this.onFileSelect.emit((event.target as any).files);
  }
}

FileSelectDirective の使い方

この FileSelectDirective は、input[type="file"] な要素に適用すれば良い。

<p>
  <input type="file" appFileSelect (onFileSelect)="onFileSelect($event)">
</p>

input[type="file"] はファイルを選択すると value 属性値が変わるので change イベントが発火する。コレをディレクティブで提供しているだけだ。

コンポーネントに用意した onFileSelect() の引数は同じく files (ファイルの配列) になるので、適宜利用すれば良い。

その他

input[type="file"] とそれを囲む「ドロップ領域」に以下のようなスタイルを当てることで、「ファイルをドロップしても良いし、領域をクリックしてファイル選択もできる」というコードを見たことがある。荒技なので避けたいところ…。

<div style="position: relative;
            width: 300px;
            height: 300px;
            border: 5px dashed #999;
            color: #999;
            font-size: 150%;
            font-weight: bold;
            text-align: center;
            line-height: 300px;
            white-space: nowrap;
            overflow: hidden;">
  Drop or Click Here
  <input type="file"
         appFileSelect (onFileSelect)="onFileSelect($event)"
         style="position: absolute;
                top: 0;
                left: 0;
                width: 300px;
                height: 300px;
                opacity: 0;
                cursor: pointer;">
</div>

input[type="file"] のサイズを広げ、opacity: 0 で透明 (非表示) にしているのがミソ。visibility: hidden と違って cursor: pointer 指定も効くので、クリックできそうな要素に見せられる。あとは FileSelectDirective で change イベントをチェックしておけば良い。