Drag & Drop Images with Preview using Stimulus Outlets

3d rendering of a handcar filled with sand and a small shovel on top.

In a previous article I explored a way to preview images before uploading with Stimulus.

I now want to extend its functionality by adding drag & drop. Along the way I am also using Stimulus outlets to tie the two functionalities together. Showcasing more advanced use of small Stimulus controllers.

I assume you walked through all the steps of the previous mentioned article.

Let’s start with the HTML. It’s using the other HTML with just a few attributes added.

<div data-controller="image-preview dropzone" data-action="dragover->dropzone#dragOver dragleave->dropzone#dragLeave drop->dropzone#drop">
  <img data-image-preview-target="canvas" hidden class="object-cover size-48">

  <input type="file" accept="image/*" data-image-preview-target="source" data-dropzone-target="input" data-action="image-preview#show" hidden>
</div>

Let’s create the dropzone_controller.js.

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["input"];

  dragOver(event) {
    event.preventDefault();
  }

  dragLeave(event) {
    event.preventDefault();
  }

  drop(event) {
    event.preventDefault();

    this.#updateInputField(event.dataTransfer.files[0]);
  }
}

All these methods do is preventing the default event when these actions are invoked. The drop() function also calls the private function this.#updateInputField(). Let’s add it.

export default class extends Controller {
  // …

  // private
  #updateInputField(file) {
    const dataTransfer = new DataTransfer();

    dataTransfer.items.add(file);

    this.inputTarget.files = dataTransfer.files;
  }
  // …
}

This will inject the dropped image into the inputTarget field. And just like that you can drag and drop images onto the element. 🤯

Preview images with outlets

An important part is missing though… the image isn’t showing which looks like a bug. Luckily with the image_preview_controller.js already done. This is a simple exercise.

First tweak the HTML with the following attributes:

<div data-controller="image-preview dropzone" data-dropzone-image-preview-outlet="#image-preview" data-action="dragover->dropzone#dragOver dragleave->dropzone#dragLeave drop->dropzone#drop" id="image-preview>
  <img data-image-preview-target="canvas" hidden class="object-cover size-48">

  <input type="file" accept="image/*" data-image-preview-target="source" data-dropzone-target="input" data-action="image-preview#show" hidden>
</div>

Added:

  • data-dropzone-image-preview-outlet="#image-preview";
  • id="image-preview".

Now two lines are needed in the dropzone_controller.js.

export default class extends Controller {
  static outlets = ["image-preview"];
  // …

  drop(event) {
    // …

    this.imagePreviewOutlet.show();

    // …
  }

  // …
}

Now when you drop an image it fires the show() function on the image_preview_controller.js (created in the previous article). 🥳

I like to use small Stimulus controllers like these that work great together.

Need an alternative to S3 to store your images?


Rails Designer has this Stimulus controller packaged with a few extras added! Get your copy today.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built for Ruby on Rails (inc. Rails 8)

  • Designed with Tailwind CSS and Enhanced with Hotwire

  • Updates for 12 months