Skip to content

Instantly share code, notes, and snippets.

@lazaronixon
Last active June 19, 2024 19:43
Show Gist options
  • Save lazaronixon/dca1b48c241422d6347f4b0c93bec739 to your computer and use it in GitHub Desktop.
Save lazaronixon/dca1b48c241422d6347f4b0c93bec739 to your computer and use it in GitHub Desktop.
Dropzone.js + Stimulus + Active Storage
import { Controller } from "stimulus"
import { DirectUpload } from "@rails/activestorage"
import Dropzone from "dropzone"
import { getMetaValue, findElement, removeElement, insertAfter } from "helpers"
Dropzone.autoDiscover = false
export default class extends Controller {
static targets = [ "input" ]
connect() {
this.dropZone = createDropZone(this)
this.hideFileInput()
this.bindEvents()
}
// Private
hideFileInput() {
this.inputTarget.disabled = true
this.inputTarget.style.display = "none"
}
bindEvents() {
this.dropZone.on("addedfile", (file) => {
setTimeout(() => { file.accepted && createDirectUploadController(this, file).start() }, 500)
})
this.dropZone.on("removedfile", (file) => {
file.controller && removeElement(file.controller.hiddenInput)
})
this.dropZone.on("canceled", (file) => {
file.controller && file.controller.xhr.abort()
})
this.dropZone.on("processing", (file) => {
this.submitButton.disabled = true
})
this.dropZone.on("queuecomplete", (file) => {
this.submitButton.disabled = false
})
}
get headers() { return { "X-CSRF-Token": 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 acceptedFiles() { return this.data.get("acceptedFiles") }
get addRemoveLinks() { return this.data.get("addRemoveLinks") || true }
get form() { return this.element.closest("form") }
get submitButton() { return findElement(this.form, "input[type=submit], button[type=submit]") }
}
class DirectUploadController {
constructor(source, file) {
this.directUpload = 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) {
removeElement(this.hiddenInput)
this.emitDropzoneError(error)
} else {
this.hiddenInput.value = attributes.signed_id
this.emitDropzoneSuccess()
}
})
}
// Private
createHiddenInput() {
const input = document.createElement("input")
input.type = "hidden"
input.name = this.source.inputTarget.name
insertAfter(input, this.source.inputTarget)
return input
}
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
findElement(this.file.previewTemplate, ".dz-upload").style.width = `${progress}%`
}
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)
}
}
// Top level...
function createDirectUploadController(source, file) {
return new DirectUploadController(source, file)
}
function createDirectUpload(file, url, controller) {
return new DirectUpload(file, url, controller)
}
function createDropZone(controller) {
return new Dropzone(controller.element, {
url: controller.url,
headers: controller.headers,
maxFiles: controller.maxFiles,
maxFilesize: controller.maxFileSize,
acceptedFiles: controller.acceptedFiles,
addRemoveLinks: controller.addRemoveLinks,
autoQueue: false
})
}
export function getMetaValue(name) {
const element = findElement(document.head, `meta[name="${name}"]`)
if (element) {
return element.getAttribute("content")
}
}
export function findElement(root, selector) {
if (typeof root == "string") {
selector = root
root = document
}
return root.querySelector(selector)
}
export function removeElement(el) {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
}
export function insertAfter(el, referenceNode) {
return referenceNode.parentNode.insertBefore(el, referenceNode.nextSibling);
}
<div class="dropzone dropzone-default dz-clickable" data-controller="dropzone" data-dropzone-max-file-size="5" data-dropzone-accepted-files="text/csv">
<%= form.file_field :file, direct_upload: true, data: { target: 'dropzone.input' } %>
<div class="dropzone-msg dz-message needsclick">
<h3 class="dropzone-msg-title">Solte o arquivo aqui ou clique para fazer o upload.</h3>
<span class="dropzone-msg-desc">Use os templates abaixo para criar o arquivo, o tamanho maximo é de 5 MiB.</span>
</div>
</div>
@lohannon
Copy link

@tfolk I also had to overwrite some method signatures to include the token and attachment name like this:

class DirectUploadController {
  constructor(source, file, token, attachmentName) {
    this.directUpload = createDirectUpload(file, source.url, token, attachmentName)
    this.source = source
    this.file = file
  }
  ...
  }

and

// Top level...
function createDirectUploadController(source, file, token, attachmentName) {
  return new DirectUploadController(source, file, token, attachmentName)
}

function createDirectUpload(file, url, token, attachmentName) {
  return new DirectUpload(file, url, token, attachmentName)
}

@tfolk
Copy link

tfolk commented May 11, 2022

@lohannon I'll give it a try. Thanks.

@elalemanyo
Copy link

Does anyone know what is the best way to display the files already uploaded in edit case? Thanks

@eliasglyptis
Copy link

eliasglyptis commented Jun 13, 2024

This implementation does not actually work! Sure it works for the new action, but in the edit action, controller can not find form elements and a never ending rigmarole of JS and Stimulus... never ending! Does't work! We waste all this time implementing basics over and over, while in reality if you don't come from money and big teams, and you're working solo on a project, it means you will spill your guts working on the basics and working on the complex.

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