Cómo usar FilePond con Active Storage de Rails
FilePond es una hermosa biblioteca JavaScript para manejar cargas de archivos escrita por Rik Schennink.
He usado esta biblioteca en varios proyectos de Ruby on Rails cuando quería una experiencia de carga fluida combinada con excelentes visuales de UI.
En este artículo, voy a explicar cómo integrar FilePond con Rails, y describiré y explicaré algunas de mis decisiones técnicas a medida que avanzamos. Este tutorial también asume conocimientos básicos de Active Storage. Si no estás familiarizado con esta parte de Rails, por favor consulta la guía oficial de Rails en https://guides.rubyonrails.org/active_storage_overview.html.
El código de demostración para este tutorial está ubicado en https://github.com/Code-With-Rails/filepond-demo. Te recomiendo que clones ese repositorio y ejecutes la demo desde allí.
Lo Que Estamos Construyendo
Para integrar FilePond con nuestra aplicación, hay dos partes:
- Primero, necesitaremos agregar la biblioteca JavaScript de FilePond y habilitarla en nuestra etiqueta HTML de entrada de archivo.
- Segundo, necesitaremos integrar FilePond con nuestra aplicación Rails. Específicamente, queremos que Active Storage maneje las cargas de archivos.
Para lograr la primera parte, usaremos JavaScript vanilla. Para la segunda parte, aprovecharemos la biblioteca JavaScript existente de Active Storage y habilitaremos las Cargas Directas. Para acomodar algunos endpoints específicos del servidor que FilePond requiere, crearemos un controlador personalizado.
Instalación de FilePond
Para este tutorial, comenzaremos con una aplicación Rails 7.0.x. Esto significa que usaremos importmap-rails para agregar nuestras dependencias de JavaScript.
bin/bundle add importmap-rails
bin/rails importmap:install
A continuación, necesitaremos agregar la dependencia de FilePond con el siguiente comando:
bin/rails importmap pin filepond
Queremos precargar FilePond, así que en el archivo config/importmap.rb, modifica el archivo así:
# Antes
pin 'filepond', to: 'https://ga.jspm.io/npm:filepond@4.30.4/dist/filepond.js'
# Después
pin 'filepond', to: 'https://ga.jspm.io/npm:filepond@4.30.4/dist/filepond.js', preload: true
Conceptos Básicos de FilePond
Para inicializar FilePond, primero necesitaremos tener un elemento HTML de entrada de archivo. Nuestro formulario se verá algo así:
<%= form_with model: @user, url: update_avatar_path, method: :post do |f| %>
<%= f.label :avatar, 'Actualiza tu avatar' %>
<!-- Transformaremos esta entrada en un pond -->
<%= f.file_field :avatar, class: 'filepond', direct_upload: true %>
<%= f.button 'Actualizar' %>
<% end %>
El código JavaScript correspondiente se verá así:
// application.js
const input = document.querySelector('.filepond')
FilePond.create(input)
Eso es el mínimo de lo que necesitas para que FilePond convierta una simple etiqueta de entrada de archivo en el widget.
Específicamente para lo que estamos construyendo aquí, implementaremos lo siguiente:
- Configurar FilePond para realizar cargas directas (usando la biblioteca JavaScript de Active Storage) a proveedores de nube
- Configurar FilePond y nuestra aplicación para permitir la carga solo con una URL remota
- Configurar nuestra aplicación para purgar archivos no adjuntos (blobs) cuando los usuarios hacen clic en cancelar (deshacer) en el widget de FilePond
Aplicación Rails Simple
Primero configuraremos nuestro modelo de datos para que Active Storage pueda asociar archivos adjuntos.
bin/rails g model User name:string
bin/rails db:migrate
En el archivo user.rb, actualicemos el modelo a esto:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
Integrando FilePond
Configuremos FilePond para usar la función de Carga Directa de Active Storage:
// 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(`Algo salió mal: ${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
}
}
})
Para los endpoints de fetch y revert, necesitamos un controlador personalizado:
# 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
Y las rutas:
# config/routes.rb
Rails.application.routes.draw do
post 'filepond/fetch', to: 'filepond#fetch'
delete 'filepond/remove', to: 'filepond#remove'
end
Conclusión
FilePond ofrece una excelente interfaz de usuario para proporcionar retroalimentación a las cargas de usuarios. Integrarlo con Active Storage de Rails no es difícil, pero requiere un poco de personalización.
Este tutorial presenta una forma de integrar FilePond y aplica Rails vanilla tanto como sea posible. En la siguiente parte, usaré los métodos de implementación anteriores y convertiré todo esto en una gema que podemos reutilizar sin tener que reimplementar esto cada vez.