Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails ActiveStorage: DirectUpload callbacks

I am having some trouble with working directly with ActiveStorage's DirectUpload object. I am following examples right from RailsGuides, but I must be missing something. Here is a quick layout of my issue:

  1. What I am trying to accomplish.
  2. What I have already attempted to do.
  3. What my current issues are.

1. What I am trying to accomplish

Using ActiveStroage, I am trying to allow a user to select multiple files on a simple form, and automatically initiate the direct upload after the files have been selected.

This is the form interfacing with the end user:

_media_upload_form.html.erb

<%= form_with url: elements_upload_path, local: true, id: "upload-elements" do %>
  <span class="btn btn-primary btn-file">
    <%= form.file_field :images, multiple: true, direct_upload: true %>
    Select File(s)
  </span>
<% end %>

2. What I have already attempted to do

To accomplish an automatic file upload after a user has selected files, I have to directly interface with the DirectUpload object. This hint is found right on the ActiveStroage RailsGuides. I had no issues getting this to work with the following JS code:

direct_uploads.js

import { DirectUpload } from "activestorage"

const input = document.querySelector('input[type=file]')

const onDrop = (event) => {
  event.preventDefault()
  const files = event.dataTransfer.files;
  Array.from(files).forEach(file => uploadFile(file))
}

input.addEventListener('change', (event) => {
  Array.from(input.files).forEach(file => uploadFile(file))
  input.value = null
})

const uploadFile = (file) {
  const url = input.dataset.directUploadUrl
  const upload = new DirectUpload(file, url)

  upload.create((error, blob) => {
    if (error) {
      // Handle the error
    } 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)
    }
  })
}

So, I accomplished one goal. I had files uploading as soon as they were being selected. Now, my next goal was to access events, so I know when uploads are complete, progress, etc. It is especially important to know when uploads are complete so I can submit the form and have objects created and attached to the uploaded files. So, using something like this:

addEventListener("direct-upload:progress", event => {
  // ...
})

Will not work, since I am accessing the DirectUpload object directly. At least, that has been my experience so far. A little puzzled as to why, I noticed a detail (that I originally overlooked) in ActiveStroage RailsGuides that says you can bind handlers by creating your own DirectUpload upload class. So, using the example provided in the guide, I created the following:

my_uploader.js

import { DirectUpload } from "activestorage"

class MyUploader {
  constructor(file, url) {
    this.upload = new DirectUpload(this.file, this.url, this)
  }

  upload(file) {
    this.upload.create((error, blob) => {
      if (error) {
        // Handle the error
      } 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)
      }
    })
  }

  directUploadWillStoreFileWithXHR(request) {
    request.upload.addEventListener("progress",
      event => this.directUploadDidProgress(event))
  }

  directUploadDidProgress(event) {
    console.log("Upload has some progress ....")
  }
}

// ... all ES6 export calls ...

direct_uploads.js

import { DirectUpload } from "activestorage"
import { MyUploader } from "my_uploader"

const input = document.querySelector('input[type=file]')

const onDrop = (event) => {
  event.preventDefault()
  const files = event.dataTransfer.files;
  Array.from(files).forEach(file => uploadFile(file))
}

input.addEventListener('change', (event) => {
  Array.from(input.files).forEach(file => uploadFile(file))
  input.value = null
})

const uploadFile = (file) {
  const url = input.dataset.directUploadUrl
  const upload = new MyUploader(file, url)
}

3. What my current issues are

I think my issue is that I am missing something, a step perhaps. The MyUploader constructor is being called, however files are no longer being uploaded. Only the constructor is being called and that is it. The actual upload processes is no longer being called. I am lost on how to get the custom MyUploader to continue with the upload process, just as the DirectUpload object does.

Any direction anyone can provide would be greatly appreciated.

Thank you!

like image 385
Matt D Avatar asked May 27 '18 01:05

Matt D


1 Answers

After leaving my project for a while and coming back with fresh eyes, I saw the answer.

DirectUpload can take three parameters, the third being a MyUploader object.

So the answer is something like:

const my_uploader = new MyUploader(file, url)
const upload = new DirectUpload(file, url, my_uploader)
like image 147
Matt D Avatar answered Sep 28 '22 15:09

Matt D