Alternative approach to drag and drop sorting with acts_as_list

Curated ago by @jeremysmithco

Description

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.

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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';
  }
}