RailsのActive StorageでFilePondを使う方法

FilePondは、Rik Schenninkによって書かれた、ファイルアップロードを処理するための美しいJavaScriptライブラリです。

私は、スムーズなアップロード体験と優れたUIビジュアルを求める様々なRuby on Railsプロジェクトでこのライブラリを使用してきました。

この記事では、FilePondをRailsと統合する方法を説明し、進めながら技術的な選択について説明します。このチュートリアルはActive Storageの基本的な知識を前提としています。Railsのこの部分に馴染みがない場合は、https://guides.rubyonrails.org/active_storage_overview.htmlの公式Railsガイドをご覧ください。

このチュートリアルのデモコードはhttps://github.com/Code-With-Rails/filepond-demoにあります。そのリポジトリをクローンして、そこからデモを実行することをお勧めします。

何を構築するか

FilePondをアプリに統合するには、2つの部分があります:

  • まず、FilePond JavaScriptライブラリを追加し、ファイル入力HTMLタグで有効にする必要があります。
  • 次に、FilePondをRailsアプリと統合する必要があります。具体的には、Active Storageにファイルアップロードを処理させたいです。

最初の部分を達成するために、バニラJavaScriptを使用します。2番目の部分では、既存のActive StorageのJavaScriptライブラリを活用し、ダイレクトアップロードを有効にします。FilePondが必要とする特定のサーバーエンドポイントに対応するために、カスタムコントローラを作成します。

FilePondのインストール

このチュートリアルでは、Rails 7.0.xアプリケーションから始めます。これは、JavaScript依存関係を追加するためにimportmap-railsを使用することを意味します。

bin/bundle add importmap-rails
bin/rails importmap:install

次に、以下のコマンドでFilePondの依存関係を追加する必要があります:

bin/rails importmap pin filepond

FilePondをプリロードしたいので、config/importmap.rbファイルを次のように修正します:

# 変更前
pin 'filepond', to: 'https://ga.jspm.io/npm:filepond@4.30.4/dist/filepond.js'

# 変更後
pin 'filepond', to: 'https://ga.jspm.io/npm:filepond@4.30.4/dist/filepond.js', preload: true

FilePondの基本

FilePondを初期化するには、まずHTMLファイル入力要素が必要です。フォームは次のようになります:

<%= form_with model: @user, url: update_avatar_path, method: :post do |f| %>
  <%= f.label :avatar, 'アバターを更新' %>
  <!-- この入力をpondに変換します -->
  <%= f.file_field :avatar, class: 'filepond', direct_upload: true %>
  <%= f.button '更新' %>
<% end %>

対応するJavaScriptコードは次のようになります:

// application.js
const input = document.querySelector('.filepond')
FilePond.create(input)

これが、FilePondがシンプルなファイル入力タグをウィジェットに変換するために必要な最小限のものです。

ここで構築しているものに特化して、以下を実装します:

  • FilePondを設定して(Active StorageのJavaScriptライブラリを使用して)クラウドプロバイダーへのダイレクトアップロードを実行
  • FilePondとアプリを設定してリモートURLのみでのアップロードを許可
  • ユーザーがFilePondウィジェットでキャンセル(元に戻す)をクリックしたときに未添付ファイル(blob)をパージするようにアプリを設定

シンプルなRailsアプリケーション

まず、Active Storageがファイル添付を関連付けられるようにデータモデルをセットアップします。

bin/rails g model User name:string
bin/rails db:migrate

user.rbファイルで、モデルを次のように更新しましょう:

class User < ApplicationRecord
  has_one_attached :avatar do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end

FilePondの統合

Active Storageのダイレクトアップロード機能を使用するようにFilePondを設定しましょう:

// app/javascript/application.js
FilePond.setOptions({
  server: {
    process: (fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
      const uploader = new DirectUpload(file, directUploadUrl, {
        directUploadWillStoreFileWithXHR: (request) => {
          request.upload.addEventListener(
            'progress',
            event => progress(event.lengthComputable, event.loaded, event.total)
          )
        }
      })

      uploader.create((errorResponse, blob) => {
        if (errorResponse) {
          error(`エラーが発生しました:${errorResponse}`)
        } else {
          const hiddenField = document.createElement('input')
          hiddenField.setAttribute('type', 'hidden')
          hiddenField.setAttribute('value', blob.signed_id)
          hiddenField.name = input.name
          document.querySelector('form').appendChild(hiddenField)
          load(blob.signed_id)
        }
      })

      return { abort: () => abort() }
    },
    fetch: {
      url: './filepond/fetch',
      method: 'POST'
    },
    revert: {
      url: './filepond/remove'
    },
    headers: {
      'X-CSRF-Token': document.head.querySelector("[name='csrf-token']").content
    }
  }
})

fetchとrevertエンドポイントには、カスタムコントローラが必要です:

# app/controllers/filepond_controller.rb
require 'open-uri'

class FilepondController < ApplicationController
  def fetch
    uri = URI.parse(raw_post)
    url = uri.to_s

    blob = ActiveStorage::Blob.create_and_upload!(
      io: URI.open(uri),
      filename: URI.parse(url).path.parameterize
    )

    if blob.persisted?
      redirect_to rails_service_blob_path(blob.signed_id, blob.filename)
    else
      head :unprocessable_entity
    end
  end

  def remove
    signed_id = raw_post
    blob = ActiveStorage::Blob.find_signed(signed_id)

    if blob
      blob.purge
      head :ok
    else
      head :not_found
    end
  end

  private

  def raw_post
    request.raw_post
  end
end

そしてルート:

# config/routes.rb
Rails.application.routes.draw do
  post 'filepond/fetch', to: 'filepond#fetch'
  delete 'filepond/remove', to: 'filepond#remove'
end

結論

FilePondは、ユーザーのアップロードにフィードバックを提供するための優れたユーザーインターフェースを提供します。RailsのActive Storageとの統合は難しくありませんが、少しのカスタマイズが必要です。

このチュートリアルは、FilePondを統合する方法の一つを提示し、できるだけバニラRailsを適用しています。次のパートでは、上記の実装方法を使用して、これをすべてgemに変換し、毎回再実装することなく再利用できるようにします。