Stimulusを使ってドラッグ&ドロップでファイルアップロード


Rails で input[type=file] の入力欄にファイルをドラッグ&ドロップして選択する機能を追加しようと調べてみたのですが、script 要素にべた書きの実装例しか見つからなかったので、Stimulus を使った実装をしてみました。

環境

  • Ruby 3.1.1
  • Ruby on Rails 7.0.2

View

既存のページには、data-controller 属性と data-action 属性と data-*-target 属性を追加するだけなので、ほとんで手を入れずに済みます。

Stimulus 側に file_drop という名前のコントローラを作るので、data-* 属性の値や名前もそれに合わせます。

app/views/users/_form.html.erb
  ...
  <div data-controller="file-drop"
      data-action="dragover->file-drop#dragover dragleave->file-drop#dragleave drop->file-drop#drop">
    <%= form.label :image, style: "display: block" %>
    <%= form.file_field :image, 'data-file-drop-target': 'fileUpload' %>
  </div>
  ...

Controller

targets で指定した値(fileUpload)から自動的に fileUploadTarge というプロパティが生成されます。
これは自動で生成されるため、targets の値に file-upload のようなケバブケースは使えません。
'data-file-drop-target': 'file-upload' って書きたかったのですが諦めました。

data-action 属性でイベントとそれに対応するコントローラ#メソッドを指定したものが、ここで呼ばれます。
element.addEventListener('dragover', () => {}) としていたのが、ちょっとわかりやすく書けます。

app/javascript/controllers/file_drop_controller.js
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static targets = ['fileUpload']

  dragover(e) {
    e.preventDefault()

    // dragover したときに border の色を変える
    this.fileUploadTarget.classList.add('border-primary')
  }

  dragleave(e) {
    e.preventDefault()
    this.fileUploadTarget.classList.remove('border-primary')
  }

  drop(e) {
    e.preventDefault()
    this.fileUploadTarget.classList.remove('border-primary')

    const files = e.dataTransfer.files
    if (typeof files[0] !== 'undefined') {
      this.fileUploadTarget.files = files
    }
  }
}

あとは作成したコントローラを読み込むだけ。

app/javascript/controllers/index.js
import { application } from './application'

import FileDropController from './file_drop_controller'
application.register('file-drop', FileDropController)