Note: This post will eventually be part of a series about a complete solution for users to upload and manage multiple images that are attached to a record.
At this point, you should have image hosting set up, you can upload multiple images, and they should persist when editing the record.
Next, we want to add the ability to re-arrange the order of the images and for the order to persist.
To do this we are going to use a few tools:
Importmaps, stimulus, RequestJS, acts_as_list gem, and a js library called sortable_js.
We will start off by setting up all of the required dependencies.
acts_as_list setup
https://github.com/brendon/acts_as_list
Add
gem 'acts_as_list', '~> 1.0.4'
to your Gemfile, and runbundle install
Add a position column to ActiveStorageAttachments table.
rails g migration AddPostionColumnToActiveStorageAttachments position:integer
.
This is what the acts_as_list gem will use to manage the order of the images.
- Crate an initializer so act_as_list can access the ActiveStorage Model
# config/initializers/active_storage_acts_as_list.rb
module ActiveStorageAttachmentList
extend ActiveSupport::Concern
included do
acts_as_list scope: %i[record_id record_type name]
default_scope { order(:position) }
end
end
Rails.configuration.to_prepare do
ActiveStorage::Attachment.send :include, ActiveStorageAttachmentList
end
Don't forget to restart the server whenever you add or update your initializers.
sortable_js setup
https://github.com/rails/requestjs-rails
- Run
bin/importmap pin sortablejs
- Create the stimulus controller. run
rails g stimulus sortable
- Setup the controller
// app/assets/javascript/controllers/sortable_controller.js
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs" // Add this line, to access the sortablejs library
// Connects to data-controller="sortable"
export default class extends Controller {
connect() {
console.log("Sortable controller connected") // I add this for testing purposes. Once everything is working you can comment this line out.
}
}
- Connect the view to the controller
# app/views/cars/_form.html.erb
<div class="images-wrapper" data-controller="sortable" >
<% @car.images.each do |image| %>
<div class="form-image-card">
<%= image_tag image, class:"form-image" %>
</div>
<% end %>
</div>
Now when you load the page, in the browser console, you should see Sortable controller connected
RequestJS Set up
https://github.com/rails/requestjs-rails
- Add the requestjs-rails gem to your Gemfile: gem 'requestjs-rails'
- Run ./bin/bundle install.
- Run ./bin/rails requestjs:install
- Add
import { FetchRequest } from '@rails/request.js'
to the sortable controller.
// app/assets/javascript/controllers/sortable_controller.js
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
import { FetchRequest } from '@rails/request.js'
// Connects to data-controller="sortable"
export default class extends Controller {
...
And that's it for setting up our dependencies. Now it's on to the code.
Adding drag and drop functionality
In our sortable
stimulus controller, we want to connect the parent element that contains the images we want to sort. So in our view file, we already have:
# app/views/cars/_form.html.erb
<div class="images-wrapper" data-controller="sortable" >
<% @car.images.each do |image| %>
<div class="form-image-card">
<%= image_tag image, class:"form-image" %>
</div>
<% end %>
</div>
Now we want to create a sortable element out of the "images-wrapper" div.
// app/assets/javascript/controllers/sortable_controller.js
...
// Connects to data-controller="sortable"
export default class extends Controller {
connect() {
console.log("Sortable controller connected") # I add this for testing purposes. Once everything is working you can comment this out.
// 'this' is the element in the view that is connecting to this controller.
this.sortable = Sortable.create(this.element, {
animation: 150,
})
}
}
Now, back in your view, you should now be able to drag and drop the images, to change their order. Huzzah! The only issue is that it doesn't persistent yet, so as soon as you reload the page the image order won't have saved.
Persisting the order
This is where acts_as_list gem comes into play. To start off, we will create a route that we will use to trigger the controller action that will save the updated order of the images when an image has been dragged and dropped.
routes
# config/routes.rb
resources cars do
member do
patch :move_image
end
end
This will give us a path of /cars/:id/move_image
that will run the action move_image
in the car
controller.
controller
Next, we will set up the controller by adding the following action
# app/controllers/cars_controller.rb
...
# /cars/:id/move_image
def move_image
#Find the car who's images we are re-arranging
@car= GeneralListing.find(params[:id])
#Find the image we are moving
@image = @car.images[params[:old_position].to_i - 1]
# Use the insert_at method we get from acts_as_list gem
@image.insert_at(params[:new_position].to_i)
head :ok
end
...
stimulus controller
When an image is dropped we want our stimulus controller to send the :move_image patch request.
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
import { FetchRequest } from '@rails/request.js'
// Connects to data-controller="sortable"
export default class extends Controller {
static values = {
url: String,
test: String
}
connect() {
console.log("Sortable controller connected")
this.sortable = Sortable.create(this.element, {
animation: 150,
ghostClass: "sortable-ghost",
chosenClass: "sortable-chosen",
dragClass: "sortable-drag",
onEnd: this.end.bind(this)
})
}
async end(event) {
console.log("Sortable onEnd triggered")
const request = new FetchRequest('patch', `${this.urlValue}?old_position=${event.oldIndex + 1}&new_position=${event.newIndex + 1}`)
const response = await request.perform()
console.log(response)
}
}
and that should be it. The user should now be able to drag and drop images, and the order should persist.
Starting out in Rails can be intimidating so if I've missed anything or if you are unsure of any of the steps, comment below and I will update the post for clarity.
References
Gorails Tutorial
https://gorails.com/episodes/sortable-drag-and-drop
https://github.com/gorails-screencasts/sortable-drag-and-drop
Gorails Forum
https://gorails.com/forum/sorting-images-using-active-storage
Drifting Rails Tutorial - Setting up Import Maps
https://www.driftingruby.com/episodes/importmaps-in-rails-7
Code with Pete Tutorial
https://www.youtube.com/watch?v=FKAMRLQpypk
Request JS
https://www.youtube.com/watch?v=ACChhA4GdfM
Request JS
https://github.com/rails/requestjs-rails
Adding acts_as_list to active storage
https://gist.github.com/kwent/050b0a580fa635e5aaa225ea3a1dd846
Sortable JS
https://sortablejs.github.io/Sortable/
Top comments (0)