Drag and Drop Sortable items wit SortableJS and StimulusJS Rails
How to create drag&drop sortable reorder of items based on Stimulus JS & Sortable JS (bonus: works under TailwindCSS)
My solution is based on Rapid Ruby's video drag and drop sortable list using Hotwire (source code) but with more straight forward urls + works out of the box with Rails 7 importmaps
Note: there's a mirror article published on medium.com
Step 0 - DB
I will assume we work with Entry model that has #title and #position where the later represents the position of the entry where first item has value of 0 .
You can achieve this with acts_as_list gem
# Gemfile # ... gem "acts_as_list"
Declaring it on a model
class Entry < ApplicationRecord # ... acts_as_list top_of_list: 0 end
given Schema
create_table "entries", force: :cascade do |t| t.string "title" t.integer "position" #... end
Step 1 - add importmaps dependensies
$ cd my_rails_project $ bin/importmap pin sortablejs @rails/request.js
which will add :
// config/importmap.rb // ... pin "sortablejs", to: "https://ga.jspm.io/npm:[email protected]/modular/sortable.esm.js" pin "@rails/request.js", to: "https://ga.jspm.io/npm:@rails/[email protected]/src/index.js"
Note1: no, you don't have to install requestjs-rails gem. Pinned JS is enough
Note2: no you don't need to add anything to app/javascript/application.js
Step 2 - Stimulus controller
// app/javascript/controllers/sortable_controller.js import { Controller } from "@hotwired/stimulus" import Sortable from "sortablejs"; import { put } from "@rails/request.js"; //<ul data-controller="sortable"> // <li data-sortable-url="/entries/211/positions"><i data-sortable-handle>(H)</i> one</li> // <li data-sortable-url="/entries/222/positions"><i data-sortable-handle>(H)</i> two</li> // <li data-sortable-url="/entries/233/positions"><i data-sortable-handle>(H)</i> three</li> //</ul> export default class extends Controller { static values = { url: String }; connect() { this.sortable = Sortable.create(this.element, { animation: 350, ghostClass: "bg-gray-200", onEnd: this.onEnd.bind(this), handle: "[data-sortable-handle]", }); } disconnect() { this.sortable.destroy(); } onEnd(event) { const { newIndex, item } = event; const url = item.dataset["sortableUrl"] put(url, { body: JSON.stringify({ position: newIndex }) }); } }
Step 3 - ERB, Controller, Routes
When you move item it will send PUT request to data-sortable-url (whatever that is) with new position of that Entry. First element in the list has position 0
I like to have separate controller to handle position setting. This is up to you what routes/contollers you set.
<ul data-controller="sortable"> <% Entry.order(:position).each do |entry| %> <li data-sortable-url="<%= entry_positions_path(entry) %>"><i data-sortable-handle>(H)</i> <%= entry.title %></li> <% end %> </ul>
# config/routes.rb # ... resources :entries, only: [] do resource :positions, only: [:update], module: :entries end
# app/controllers/entries/positions_controller.rb class Entries::PositionsController < ApplicationController def update @entry = Entry.find(params[:entry_id]) # @entry = current_user.entries.find(params[:entry_id]) # if you use Authentication (e.g Devise) @entry.insert_at(params[:position].to_i) head :no_content end end
Other solutions & notes
- There is a stimulus-sortable component out there. I've tried to implement it but had no luck
- Rapid Ruby has a video on drag and drop sortable list using Hotwire ( source code ). My solution is 90% copy of his solution so definitely worth checking out
One more thing if you are getting 401 error when executing request.js put requests make sure you are signed in. Yes it sounds obvious but guess who spent 2 hours debugging request headers before he realised he is not signed in