Here’s an somewhat unconventional approach to drag and drop sorting using acts_as_list I came up with this week. In the old days, I would have implemented something similar to this RailsCasts episode. But I think I might like this better.
acts_as_list
I made a YouTube video explaining my thinking.
As Chris Oliver notes, it’s probably best to use Signed Global IDs to avoid users tampering with model names and IDs.
import { Controller } from '@hotwired/stimulus'; import { Sortable } from '@shopify/draggable'; import { FetchRequest } from "@rails/request.js"; export default class extends Controller { static values = { url: String }; connect() { this.sortable = new Sortable(this.element, { draggable: '.sortable-draggable', distance: this.distance, mirror: { constrainDimensions: true, }, classes: { 'droppable:occupied': 'droppable-occupied', 'source:dragging': ['opacity-60'], 'mirror': ['!border-blue-500', 'shadow-lg', 'z-40'], }, }); this.sortable.on('sortable:stop', (event) => { if (event.data.newIndex == event.data.oldIndex) return; const sortableElements = this.sortable.getSortableElementsForContainer(event.newContainer); const item = sortableElements[event.data.newIndex]; const precursor = sortableElements[event.data.newIndex - 1]; this.post(item, precursor); }); } post(item, precursor) { let formData = new FormData(); if (item) { formData.append('item', item.dataset.sortableGid) }; if (precursor) { formData.append('precursor', precursor.dataset.sortableGid) }; const request = new FetchRequest('post', this.urlValue, { body: formData }); request.perform(); } disconnect() { this.sortable.destroy(); } // Distance must be disabled for Capybara drag_to to succeed // From Draggable docs: // The distance you want the pointer to have moved before drag starts. // This can be useful for clickable draggable elements, such as links. get distance() { return (this.isTestEnvironment ? 0 : 10); } get isTestEnvironment() { const element = document.head.querySelector('meta[name=environment]'); return element && element.content == 'test'; } }