How to use FilePond with Rails' Active Storage

ruby-on-railsjavascript

FilePond is a beautiful JavaScript library for handling file uploads written by Rik Schennink.

I’ve used this library in various Ruby on Rails projects whenever I wanted a smooth upload experience paired with great UI visuals.

In this article, I’m going to go over how to integrate FilePond with Rails, and will describe and explain some of my technical choices as I go along. This tutorial also assumes some basic knowledge of Active Storage. If you are not familiar with this part of Rails, please have a look at the official Rails guide at https://guides.rubyonrails.org/active_storage_overview.html.

The demo code for this tutorial is located at https://github.com/Code-With-Rails/filepond-demo. I recommend that you clone that repository and run the demo from there.

What We Are Building

To integrate FilePond with our app, there are two parts:

  • First, we will need to add the FilePond JavaScript library and enable it on our file input HTML tag.
  • Second, we will need to integrate FilePond with our Rails app. Specifically, we want Active Storage to handle the file uploads.

To accomplish the first part, we will use vanilla JavaScript. For the second part, we will leverage the existing Active Storage’s JavaScript library and enable Direct Uploads. To accommodate some specific server endpoints that FilePond requires, we will create a custom controller.

FilePond Installation

For this tutorial, we’ll be starting with a Rails 7.0.x application. This means we’ll be using importmap-rails to add our JavaScript dependencies.

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

Next, we will need to add the dependency for FilePond with the following command:

bin/rails importmap pin filepond

We want to preload FilePond, so in the config/importmap.rb file, modify the file like so:

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

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

FilePond Basics

To initialize FilePond, we will need to first have an HTML file input element. Our form will look something like this:

<%= form_with model: @user, url: update_avatar_path, method: :post do |f| %>
  <%= f.label :avatar, 'Update your avatar' %>
  <!-- We'll transform this input into a pond -->
  <%= f.file_field :avatar, class: 'filepond', direct_upload: true %>
  <%= f.button 'Update' %>
<% end %>

The corresponding JavaScript code will look like this:

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

That is the minimum of what you need for FilePond to convert a simple file input tag into the widget.

Specific to what we are building here, we will implement the following:

  • Configure FilePond to perform direct uploads (using Active Storage’s JavaScript library) to cloud providers
  • Configure FilePond and our app to allow upload with a remote URL only
  • Configure our application to purge unattached files (blobs) when users click on cancel (undo) in the FilePond widget

Simple Rails application

We will first set up our data model so that Active Storage can associate file attachments.

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

In the user.rb file, let’s update the model to this:

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

Integrating FilePond

Let’s configure FilePond to use Active Storage’s Direct Upload feature:

// 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(`Something went wrong: ${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
    }
  }
})

For the fetch and revert endpoints, we need a custom controller:

# 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

And the routes:

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

Conclusion

FilePond offers a great user interface for providing feedback to user uploads. Integrating it with Rails’ Active Storage is not difficult, but requires a bit of customization.

This tutorial presents a way to integrate FilePond and applies as much vanilla Rails as possible. In the next part, I will use the implementation methods above and turn all of this into a gem that we can reuse without having to re-implement this every single time.