Skip to content

Instantly share code, notes, and snippets.

@equivalent
Last active April 8, 2024 03:00
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save equivalent/37cf2044d28d3beac4568ead28feb069 to your computer and use it in GitHub Desktop.
Save equivalent/37cf2044d28d3beac4568ead28feb069 to your computer and use it in GitHub Desktop.
Rails 7 importmaps dropzone.js direct upload ActiveStorage

This is simple implementation of technologies in hobby project of mine built in Rails7 where I need direct upload to S3.

make sure you configure CORS on the S3 bucket otherwise it will not work!!! => s3 bucket cors rails direct upload

note: JS implementation of this gist is not the best. I just copied it from some article (probably this one) and altered it so it fits into Stimulus controller with bunch of duplications. I'm gret with Ruby & Rails I'm not good with JavaScript

bin/importmap pin dropzone
-# app/views/layouts/application.html.slim
doctype html
html
head
meta content=("text/html; charset=UTF-8") http-equiv="Content-Type" /
title Moments
= csrf_meta_tags
= csp_meta_tag
= stylesheet_link_tag "application", "data-turbo-track": "reload"
= javascript_importmap_tags
meta content="width=device-width, initial-scale=1" name="viewport" /
-# I'm loading dropzone css here as a muppet. Feel free to place it to sprocket asset folder or use content_for tag
link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" type="text/css"
body
# app/helpers/application_helper.rb
module ApplicationHelper
def dropzone_controller_div
data = {
controller: "dropzone",
'dropzone-max-file-size'=>"8",
'dropzone-max-files' => "10",
'dropzone-accepted-files' => 'image/jpeg,image/jpg,image/png,image/gif',
'dropzone-dict-file-too-big' => "Váš obrázok ma veľkosť {{filesize}} ale povolené sú len obrázky do veľkosti {{maxFilesize}} MB",
'dropzone-dict-invalid-file-type' => "Nesprávny formát súboru. Iba obrazky .jpg, .png alebo .gif su povolene",
}
content_tag :div, class: 'dropzone dropzone-default dz-clickable', data: data do
yield
end
end
end
# app/models/bucket.rb
class Bucket < ApplicationRecord
validates_presence_of :title
has_many_attached :files
end
# app/controllers/buckets_controller.rb
class BucketsController < ApplicationController
before_action :set_bucket, only: %i[ show edit update destroy ]
# ...
def edit
end
# ...
private
def set_bucket
@bucket = Bucket.find(params[:id])
end
# Only allow a list of trusted parameters through.
def bucket_params
params.require(:bucket).permit( files: [])
end
end
// app/javascript/controllers/dropzone_controller.js
import { Controller } from "@hotwired/stimulus"
import { DirectUpload } from "@rails/activestorage";
import { Dropzone } from "dropzone";
export default class extends Controller {
static targets = ["input"];
connect() {
this.dropZone = this.createDropZone(this);
this.hideFileInput();
this.bindEvents();
Dropzone.autoDiscover = false;
}
// Private
hideFileInput() {
this.inputTarget.disabled = true;
this.inputTarget.style.display = "none";
}
bindEvents() {
this.dropZone.on("addedfile", file => {
setTimeout(() => {
file.accepted && this.createDirectUploadController(this, file).start();
}, 500);
});
this.dropZone.on("removedfile", file => {
file.controller && this.removeElement(file.controller.hiddenInput);
});
this.dropZone.on("canceled", file => {
file.controller && file.controller.xhr.abort();
});
}
get headers() {
return { "X-CSRF-Token": this.getMetaValue("csrf-token") };
}
get url() {
return this.inputTarget.getAttribute("data-direct-upload-url");
}
get maxFiles() {
return this.data.get("maxFiles") || 1;
}
get maxFileSize() {
return this.data.get("maxFileSize") || 256;
}
get dictFileTooBig() {
return this.data.get("dictFileTooBig") || "File sile is {{filesize}} but only files up to {{maxFilesize}} are allowed";
}
get dictInvalidFileType() {
return this.data.get("dictInvalidFileType") || "Invalid file type";
}
get acceptedFiles() {
return this.data.get("acceptedFiles");
}
get addRemoveLinks() {
return this.data.get("addRemoveLinks") || true;
}
getMetaValue(name) {
const element = this.findElement(document.head, `meta[name="${name}"]`);
if (element) {
return element.getAttribute("content");
}
}
findElement(root, selector) {
if (typeof root == "string") {
selector = root;
root = document;
}
return root.querySelector(selector);
}
removeElement(el) {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
}
createDirectUploadController(source, file) {
return new DirectUploadController(source, file);
}
createDropZone(controller) {
return new Dropzone(controller.element, {
url: controller.url,
headers: controller.headers,
maxFiles: controller.maxFiles,
maxFilesize: controller.maxFileSize,
dictFileTooBig: controller.dictFileTooBig,
dictInvalidFileType: controller.dictInvalidFileType,
acceptedFiles: controller.acceptedFiles,
addRemoveLinks: controller.addRemoveLinks,
autoQueue: false
});
}
}
class DirectUploadController {
constructor(source, file) {
this.directUpload = this.createDirectUpload(file, source.url, this);
this.source = source;
this.file = file;
}
start() {
this.file.controller = this;
this.hiddenInput = this.createHiddenInput();
this.directUpload.create((error, attributes) => {
if (error) {
this.removeElement(this.hiddenInput);
this.emitDropzoneError(error);
} else {
this.hiddenInput.value = attributes.signed_id;
this.emitDropzoneSuccess();
}
});
}
createHiddenInput() {
const input = document.createElement("input");
input.type = "hidden";
input.name = this.source.inputTarget.name;
this.insertAfter(input, this.source.inputTarget);
return input;
}
insertAfter(el, referenceNode) {
return referenceNode.parentNode.insertBefore(el, referenceNode.nextSibling);
}
removeElement(el) {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
}
directUploadWillStoreFileWithXHR(xhr) {
this.bindProgressEvent(xhr);
this.emitDropzoneUploading();
}
bindProgressEvent(xhr) {
this.xhr = xhr;
this.xhr.upload.addEventListener("progress", event =>
this.uploadRequestDidProgress(event)
);
}
uploadRequestDidProgress(event) {
const element = this.source.element;
const progress = (event.loaded / event.total) * 100;
this.findElement(
this.file.previewTemplate,
".dz-upload"
).style.width = `${progress}%`;
}
findElement(root, selector) {
if (typeof root == "string") {
selector = root;
root = document;
}
return root.querySelector(selector);
}
emitDropzoneUploading() {
this.file.status = Dropzone.UPLOADING;
this.source.dropZone.emit("processing", this.file);
}
emitDropzoneError(error) {
this.file.status = Dropzone.ERROR;
this.source.dropZone.emit("error", this.file, error);
this.source.dropZone.emit("complete", this.file);
}
emitDropzoneSuccess() {
this.file.status = Dropzone.SUCCESS;
this.source.dropZone.emit("success", this.file);
this.source.dropZone.emit("complete", this.file);
}
createDirectUpload(file, url, controller) {
return new DirectUpload(file, url, controller);
}
}
-# app/views/buckets/edit.html.slim
= form_with model: @bucket do |f|
= f.label :files, class: "label d-none"
= dropzone_controller_div do
- f.object.files.each do |file|
= f.hidden_field :files, multiple: true, value: file.signed_id
= f.file_field :files, direct_upload: true, multiple: true, 'data-dropzone-target': 'input'
.dropzone-msg.dz-message.needsclick.text-gray-600
h3.dropzone-msg-title Drag here to upload or click here to browse
span.dropzone-msg-desc.text-sm 2 MB file size maximum. Allowed file types png, jpg.
= f.button "Upload", class: 'btn-primary mt-2'
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.js"
pin "@hotwired/stimulus", to: "stimulus.js"
pin "@hotwired/stimulus-importmap-autoloader", to: "stimulus-importmap-autoloader.js"
pin_all_from "app/javascript/controllers", under: "controllers"
# Use direct uploads for Active Storage (remember to import "@rails/activestorage" in your application.js)
pin "@rails/activestorage", to: "activestorage.esm.js"
# added via `bin/importmap pin dropzone`
pin "dropzone", to: "https://ga.jspm.io/npm:dropzone@6.0.0-beta.2/dist/dropzone.mjs"
pin "just-extend", to: "https://ga.jspm.io/npm:just-extend@5.1.1/index.esm.js" # required by dropzone
<Cors>
<CorsRule>
<AllowedOrigins>http://localhost:3000</AllowedOrigins>
<AllowedMethods>PUT</AllowedMethods>
<AllowedHeaders>Origin, Content-Type, Content-MD5, x-ms-blob-content-disposition, x-ms-blob-type</AllowedHeaders>
<MaxAgeInSeconds>3600</MaxAgeInSeconds>
</CorsRule>
</Cors>
- @bucket.files.each do |file|
= image_tag(file.variant(resize_to_fill: [100, 100]))
@equivalent
Copy link
Author

alternative way how to load layout where CDN css for dropzone is loaded when we use dropzone_controller_div helper

-# app/views/layouts/application.html.slim
doctype html
html
  head
    meta content=("text/html; charset=UTF-8") http-equiv="Content-Type" /
    title Moments
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag "application", "data-turbo-track": "reload"
    = javascript_importmap_tags
    meta content="width=device-width, initial-scale=1" name="viewport" /
    
    -# load dropzone CSS 
    yield :head_link
  body
# app/helpers/application_helper.rb

module ApplicationHelper

  def dropzone_controller_div
    content_for :head_link do
      tag :link, rel: "stylesheet", href: "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css", type: "text/css"
    end

    data = {
      controller: "dropzone",
      'dropzone-max-file-size'=>"8",
      'dropzone-max-files' => "10",
      'dropzone-accepted-files' => 'image/jpeg,image/jpg,image/png,image/gif',
      'dropzone-dict-file-too-big' => "Váš obrázok ma veľkosť {{filesize}} ale povolené sú len obrázky do veľkosti {{maxFilesize}} MB",
      'dropzone-dict-invalid-file-type' => "Nesprávny formát súboru. Iba obrazky .jpg, .png alebo .gif su povolene",
    }

    content_tag :div, class: 'dropzone dropzone-default dz-clickable', data: data do
      yield
    end
  end
end

@equivalent
Copy link
Author

how it looks like
Screenshot 2022-03-04 at 23 12 54

progress

Screenshot 2022-03-04 at 23 13 02

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment