Rails 6 and multiple uploads
[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.
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: