Blankman
Blankman

Reputation: 267010

How to trigger the file upload on the client side and not on form submission?

I have a working version of the active-storage example using s3 found here:

https://edgeguides.rubyonrails.org/active_storage_overview.html

Now I want to be able to perform the file upload not when I finishing filling the form but immediately after the user selects a file to upload. Actually in my case I have a wysiwyg editor that has a on drop event that fires

var myCodeMirror = CodeMirror.fromTextArea(post_body, {
   lineNumbers: true,
   dragDrop: true
  });

  myCodeMirror.on('drop', function(data, e) {
    var file;
    var files;
    // Check if files were dropped
    files = e.dataTransfer.files;
    if (files.length > 0) {
      e.preventDefault();
      e.stopPropagation();
      file = files[0];
      console.log('File: ' + file.name);
      console.log('File: ' + file.type);
      return false;
    }
  });

So is there, since the file drop triggers this event, for me to then send this to active-storage somehow so it will start uploading the file to S3 right away?

Upvotes: 5

Views: 2749

Answers (2)

Maxim
Maxim

Reputation: 71

The main problem of the topic is - you cannot Import DataUpload in java script section of the form. But we can create object ImmediateUploader as follow:

Global Java script part

upload/uploader.js

import { DirectUpload } from "@rails/activestorage"

export default class Uploader {
  constructor(file, url) {
    this.file = file
    this.url = url
    this.directUpload = new DirectUpload(this.file, this.url, this)
  }
 
  upload() {
    return new Promise((resolve, reject) => {
      this.directUpload.create((error, blob) => {
        if (error) {
          // Handle the error
          reject(error)
        } else {
          // Add an appropriately-named hidden input to the form
          // with a value of blob.signed_id
          resolve(blob)
        }
      })
    })
  }
}

upload/index.js

import Uploader from './uploader.js'

export default {
    upload (file, url) {
        const uploader = new Uploader(file, url)
        return uploader.upload()
    }
}

application.js

window.ImmediateUploader = require('./upload');

Form part
Now we can use ImmediateUploader to upload selected files directly to active storage and update images after load without commit:


<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= f.error_notification %>

  <div class="form-inputs">
    <div class="row">
      <img id="avatar" class="centered-and-cropped" width="100" height="100" style="border-radius:50%" src="<%= url_for(user.photo) %>"> 
      <button type="button" class="btn" onclick="event.preventDefault(); document.getElementById('user_photo').click()">Change avatar</button>
    </div>
    <%= f.file_field :photo, direct_upload: true, class: "hiddenfile" %>  
  </div>
  <div class="form-actions">
    <%= f.button :submit, t(".update"), class: 'btn btn-primary'  %>
  </div>
<% end %>

<% content_for :js do %>
<script>
const input = document.querySelector('input[type=file]')

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

const uploadFile = (file) => {
  // your form needs the file_field direct_upload: true, which
  //  provides data-direct-upload-url
  const url = input.dataset.directUploadUrl;
  ImmediateUploader.default.upload (file, url)
    .then(blob => {
      // get blob.signed_id and add it to form values to submit form
      const hiddenField = document.createElement('input')
      hiddenField.setAttribute("type", "hidden");
      hiddenField.setAttribute("value", blob.signed_id);
      hiddenField.name = input.name
      document.querySelector('form').appendChild(hiddenField)
      // Update new avatar Immediately
      document.getElementById('avatar').src = '/rails/active_storage/blobs/' + blob.signed_id + '/' + blob.filename;     
      // Update photo in Database
      axios.post('/users/photo', { 'photo': blob.signed_id }).then(response => {});
    });
}</script>
<% end %>

Controller:

class RegistrationController < Devise::RegistrationsController
  def update
    super
    @user = current_user
    @user.avatar = url_for(@user.photo.variant(resize_to_limit: [300, 300]).processed) if @user.photo.attached?
    @user.save
  end
  def updatephoto
    @photo = params[:photo]
    @user = current_user
    @user.photo = @photo
    @user.save

    @user = current_user
    @user.avatar = url_for(@user.photo.variant(resize_to_limit: [300, 300]).processed) if @user.photo.attached?
    @user.save
  end
end 

Upvotes: 0

Carlos Ramirez III
Carlos Ramirez III

Reputation: 7434

Triggering uploads from the client-side

Active Storage exposes the DirectUpload JavaScript class which you can use to trigger a file upload directly from the client-side.

You can leverage this for integrations with third-party plugins (e.g. Uppy, Dropzone) or with your own custom JS code.

Using DirectUpload

The first thing you need to do is make sure that AWS S3 is set up to handle direct uploads. This requires ensuring your CORS configuration is set up properly.

Next, you simply instantiate an instance of the DirectUpload class, passing it the file to upload and the upload URL.

import { DirectUpload } from "activestorage"

// your form needs the file_field direct_upload: true, which
// provides data-direct-upload-url
const input = document.querySelector('input[type=file]')
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)

upload.create((error, blob) => { 
   // handle errors OR persist to the model using 'blob.signed_id'
})

See full documentation here: https://edgeguides.rubyonrails.org/active_storage_overview.html#integrating-with-libraries-or-frameworks

The DirectUpload#create method initiates the upload to S3 and returns with an error or the uploaded file blob.

Assuming there are no errors, the last step is to persist the uploaded file to the model. You can do this using blob.signed_id and putting it into a hidden field somewhere on the page OR with an AJAX request to update your model.

Uploading a file on drop

In the case above, to start the direct upload on the drop simply put the code above into the drop handler.

Something like this:

myCodeMirror.on('drop', function(data, e) {
   // Get the file
   var file = e.dataTransfer.files[0];

   // You need a file input somewhere on the page...
   const input = document.querySelector('input[type=file]')
   const url = input.dataset.directUploadUrl

   // Instantiate the DirectUploader object
   const upload = new DirectUpload(file, url)

   // Upload the file
   upload.create((error, blob) => { ... })
});

Using the asset pipeline

If you are just using the asset pipeline and not using a JavaScript bundler tool, then you create instances of the DirectUpload class like this

const upload = new ActiveStorage.DirectUpload(file, url)

Upvotes: 6

Related Questions