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.