DEV Community

Jude
Jude

Posted on • Edited on

Multiple Images with Rails 7: Making Images Sortable

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

  1. Add gem 'acts_as_list', '~> 1.0.4' to your Gemfile, and run bundle install

  2. 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.

  1. 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
Enter fullscreen mode Exit fullscreen mode

Don't forget to restart the server whenever you add or update your initializers.

sortable_js setup

https://github.com/rails/requestjs-rails

  1. Run bin/importmap pin sortablejs
  2. Create the stimulus controller. run rails g stimulus sortable
  3. 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.
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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>
Enter fullscreen mode Exit fullscreen mode

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

  1. Add the requestjs-rails gem to your Gemfile: gem 'requestjs-rails'
  2. Run ./bin/bundle install.
  3. Run ./bin/rails requestjs:install
  4. 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 {
   ...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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,
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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)
  }

}
Enter fullscreen mode Exit fullscreen mode

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)