Upload progress bar on Rails using Uppy

This is how I added an upload progress bar to a standard Rails form using Uppy, stimulus and turbo streams.

1. Add uppy js and css

yarn add uppy

In your stylesheet (SASS here):

@import @uppy/core/dist/style.css
@import @uppy/dashboard/dist/style.css

2. Update the form

Replace the file input with two placeholders (HAML here):

= simple_form_for @document, url: url, data: {controller: 'forms--document'} do |f|
  - if @document.errors.any?
    .alert.alert-danger
      %ul
        - @document.errors.full_messages.each do |e|
          %li= e

  =# f.input :attachment, as: :file, hint: label # commented out, we use uppy now
  #uppy-dashboard-container
  = submit_tag "Save", class: 'btn btn-default'

3. Create stimulus controller to start uppy

Something like this, that binds uppy to the containing form:

import { Controller } from "@hotwired/stimulus"
import Uppy from '@uppy/core'
import Form from '@uppy/form'
import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'

export default class extends Controller {
  connect() {
    const uppy = new Uppy({
      restrictions: {
        minNumberOfFiles: 0,
        maxNumberOfFiles: 1
      },
      autoProceed: false // if true, the upload will start immediately after a file is selected
    });
    uppy.use(Form, {
      target: this.element
    });
    uppy.use(Dashboard, {
      inline: true,
      target: '#uppy-dashboard-container',
      hideUploadButton: false
    })
    uppy.use(XHRUpload, {
      endpoint: this.element.action,
      method: this.element.method,
      formData: true,
      fieldName: 'document[attachment]',
      responseType: 'text',
      getResponseData(responseText, response) {
        // process the server response. In this case it's turbo_stream elements
        return responseText;
      }
    })

    uppy.on('upload-success', (file, response) => {
      // append the response to the body. Turbo will pick them up
      $(this.element).append(response.body);
    })

    this.uppy = window.uppy = uppy; // useful for testing
  }

  disconnect() {
    window.uppy = null;
    this.uppy.close();
  }
}

The complicated part was using the turbo stream response. Uppy expects JSON, so we tell it explicitly that it’s text, and we wrap it in a hash, that we then use to append to the body.

4. Bonus: capybara test helper

Thanks to GoRails:

def upload_file_with_uppy(file)
  input = page.driver.evaluate_script Capybara::Selenium::Node::Html5Drag::ATTACH_FILE
  input.set_file(file)
  page.driver.execute_script <<~JS, find("#uppy-dashboard-container"), input
    var el = arguments[0],
      input = arguments[1],
      file = input.files[0];

    window.uppy.addFile({
      name: file.name,
      type: file.type,
      data: file
    })
  JS
  find("button.uppy-StatusBar-actionBtn--upload").click
end