Profile picture Schedule a Meeting
c a n d l a n d . n e t

Rails 6 and multiple uploads

Dusty Candland | | rails, rails6, stimulus

Active Storage is awesome for uploading images, but handling has_many_attached needs some additional stuff if you want be able to add and remove specific images.

This is how I setup multiple uploads so files can be added and deleted without re-uploading the files.

Model

The model is pretty easy... Probably you already have something like this:

...

  has_many_attached :images

Controller

Next we need to handle params a bit differently for updates vs creates. Also need to allow a remove_image_ids params array so we know which images/files to delete.

In the controller we need a new params method that removes the remove_image_ids and images keys for updates. Don't forget to update the call on the @product.update method call.

I also added two more methods to return the ids and images.

If the update is successful, then I remove any images that should be removed and add newly uploaded images to the current collection of images.

def update
  respond_to do |format|
    if @product.update(product_update_params) # CHANGE THIS
      remove_ids = Array.wrap(remove_image_ids_params)
      remove_ids.each do |id|
        image = @product.images.where(id: id).first
        image&.purge_later
      end

      @product.images.attach(images_params) if images_params.present?

      format.html { redirect_to @product, notice: "Product was successfully updated." }
      format.json { render :show, status: :ok, location: @product }
    else
      format.html { render :edit }
      format.json { render json: @product.errors, status: :unprocessable_entity }
    end
  end
end

...

private

def product_params
  params.require(:product).permit(:name, :description, :url,
    :brand_id, :external_id, :price, :feat_image, remove_image_ids: [], images: [])
end

def product_update_params
  product_params.except(:remove_image_ids, :images)
end

def remove_image_ids_params
  product_params[:remove_image_ids]
end

def images_params
  product_params[:images]
end

Views

In the form we need to show what images are currently uploaded. We'll use StimulusJS to select the images to be removed and set a hidden id value for the selected images. The data- attributes are for StimulusJS.

/ app/views/products/_form.html.slim

.form-group
  .existing.d-flex.flex-wrap.py-4

    - @product.images.each do |image|
      .image.mb-2 data-controller="image-manager" data-image-manager-id=image.id
        button class="border-info m-2 p-0" style="border-width: 3px" data-action="image-manager#toggle" data-target="image-manager.button"
          = image_tag image.variant(combine_options: {resize: "150x150", gravity: "Center", extent: "150x150"}), class: ""
        = hidden_field_tag "product[remove_image_ids][]", nil, data: { target: "image-manager.input" }

    - if @product.images.attached?
      span class="hint w-100"
        | Select images you want to remove

  = f.label :images, "Add Images", class: "form-control-label"
  = f.file_field :images, multiple: true, class: "form-control"

StimulusJS

This controller will attach to each image on the form and toggle it's removal status on click. This assumes you have StimulusJS setup and working already.

// app/javascript/controllers/image_manager_controller.js

import { Controller } from 'stimulus'

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

  toggle(e) {
    e.preventDefault()

    const id = this.data.get("id")

    this.buttonTarget.classList.remove("border-red")
    this.buttonTarget.classList.remove("border-info")

    if (this.inputTarget.value === id) {
      this.inputTarget.value = null
      this.buttonTarget.classList.add("border-info")
    } else {
      this.inputTarget.value = id
      this.buttonTarget.classList.add("border-red")
    }
  }
}

That's it! Now we can add multiple images without overwriting any already uploaded images. And we can remove specific uploaded images, one by one.

Webmentions

These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: