How to use FilePond with Rails' Active Storage
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.