Jordan Lee
Jan 2026
2 tabs
import { Controller } from "@hotwired/stimulus"
import { DirectUpload } from "@rails/activestorage"
export default class extends Controller {
static targets = ["input", "preview", "status"]
static values = {
url: String,
maxSize: { type: Number, default: 5242880 }, // 5MB
accept: { type: Array, default: ["image/jpeg", "image/png", "image/gif"] }
}
connect() {
this.element.addEventListener("dragover", (e) => this.dragover(e))
this.element.addEventListener("dragleave", (e) => this.dragleave(e))
this.element.addEventListener("drop", (e) => this.drop(e))
}
dragover(event) {
event.preventDefault()
this.element.classList.add("dragover")
}
dragleave(event) {
event.preventDefault()
this.element.classList.remove("dragover")
}
drop(event) {
event.preventDefault()
this.element.classList.remove("dragover")
const files = Array.from(event.dataTransfer.files)
this.uploadFiles(files)
}
selectFiles() {
const files = Array.from(this.inputTarget.files)
this.uploadFiles(files)
}
uploadFiles(files) {
files.forEach(file => {
if (!this.acceptValue.includes(file.type)) {
this.showError(`${file.name} has unsupported type`)
return
}
if (file.size > this.maxSizeValue) {
this.showError(`${file.name} exceeds maximum size`)
return
}
this.uploadFile(file)
})
}
uploadFile(file) {
const upload = new DirectUpload(file, this.urlValue)
this.showStatus(`Uploading ${file.name}...`)
upload.create((error, blob) => {
if (error) {
this.showError(`Upload failed: ${error}`)
} else {
this.showPreview(file, blob)
this.showStatus("Upload complete!")
}
})
}
showPreview(file, blob) {
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => {
const img = document.createElement('img')
img.src = e.target.result
img.className = 'w-24 h-24 object-cover rounded'
this.previewTarget.appendChild(img)
}
reader.readAsDataURL(file)
}
}
showStatus(message) {
this.statusTarget.textContent = message
this.statusTarget.className = "text-sm text-blue-600"
}
showError(message) {
this.statusTarget.textContent = message
this.statusTarget.className = "text-sm text-red-600"
}
}
<%= form_with model: @post do |f| %>
<div class="form-group">
<%= f.label :images, "Upload Images" %>
<div class="dropzone border-2 border-dashed rounded-lg p-8 text-center"
data-controller="dropzone"
data-dropzone-url-value="<%= rails_direct_uploads_url %>"
data-dropzone-accept-value='["image/jpeg", "image/png", "image/gif"]'>
<div class="mb-4">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400"></i>
<p class="text-gray-600 mt-2">Drag and drop images here</p>
<p class="text-sm text-gray-500">or click to browse</p>
</div>
<%= f.file_field :images,
multiple: true,
accept: "image/*",
class: "hidden",
data: {
dropzone_target: "input",
action: "change->dropzone#selectFiles"
} %>
<button type="button"
class="btn btn-secondary"
onclick="this.previousElementSibling.click()">
Choose Files
</button>
<div data-dropzone-target="status" class="mt-4"></div>
<div data-dropzone-target="preview" class="flex flex-wrap gap-2 mt-4"></div>
</div>
</div>
<%= f.submit class: "btn btn-primary mt-4" %>
<% end %>
2 files · javascript, erb
Explain with highlit
Modern file uploads should support drag-and-drop in addition to traditional file inputs. I use Stimulus to handle dragover, drop, and paste events, showing upload previews and progress. The controller prevents default browser behavior for drag events and extracts files from the DataTransfer object. For images, I generate preview thumbnails using FileReader API before upload. The actual upload happens via AJAX to a dedicated endpoint that returns Turbo Stream updates. This pattern works for profile pictures, attachments, or bulk file uploads. I also handle paste events to support clipboard uploads and provide clear error messages for unsupported file types or oversized files.
Related snips
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
<select name="status" class="rounded border p-2">
<option value="">Any</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
Filter UI that syncs query params via Stimulus (no front-end router)
rails
hotwire
stimulus
by Henry Kim
2 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
javascript
promises
async-await
by Alex Chang
1 tab
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"
export default class extends Controller {
connect() {
// Global shortcuts
Keyboard shortcuts with Stimulus and Mousetrap
stimulus
javascript
ux
by Jordan Lee
2 tabs
javascript
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
rails
hotwire
stimulus
by codesnips
4 tabs
javascript
// Get canvas and context
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 1. Basic shapes
// Rectangle (filled)
Canvas API for graphics and animations
canvas
javascript
graphics
by Alex Chang
1 tab
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
html
forms
validation
by Alex Chang
1 tab
Share this code
Here's the card — post it anywhere.