Quick drag and drop sorting with Rails using stimulus and shopify/draggable

In this post, you'll learn how to implement drag-and-drop sorting in Rails using StimulusJS and the Shopify Draggable library. We'll set up a Stimulus controller to handle drag events and make requests to the server to update the order.

Stimulus Controller Logic

Here is the Stimulus controller that enables the sorting:

import {Controller} from "@hotwired/stimulus"
import {Sortable} from '@shopify/draggable';

// Connects to data-controller="sortable"
export default class extends Controller {
  static classes = [ "draggable", "handle" ]
  static targets = [ "item" ]
  static values = { url: String }

  connect() {
    this.sortable = new Sortable(this.element, {
      draggable: this.draggableClass,
      handle: this.handleClass,
    })

    this.sortable.on('drag:stopped', async (e) => {
      await fetch(this.urlValue, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          order: this.itemTargets.reduce((obj, item, i) => {
            obj[item.dataset.id] = i
            return obj;
          }, {}),
        }),
      })
    });
  }
}

The controller registers itself and defines the draggable items .item and handles .handle. When initialized, it creates a new Sortable instance to handle drag events.

The key logic is on drag stop - it stringifies the updated order mapping of IDs to positions and sends a PATCH request to the configured API endpoint to update the order.

Usage in Views

We register the sortable controller and give it the draggable and handle classes. We'll also pass down the URL to which the sortable controller should send POST requests when the sort order of the list changes.

<ul data-controller="sortable" data-sortable-url-value="/api/v1/project_tasks/sort?project_id=<%= project.id %>" data-sortable-draggable-class=".item" data-sortable-handle-class=".handle">
  <% project.tasks.order(:position).each do |task| %>
    <%= render 'project_tasks/project_task', task: task %>
  <% end %>
</ul>

Server-Side Order Updating

On the backend, the controller sends an object with the updated IDs and positions. We can process this to reorder the associated models:

class Api::V1::ProjectTasksController < Api::BaseController
  # This method is used for drag and drop sorting of project
  # tasks.
  #
  #  `project_id` : string It accepts the project_id as a safety mechanism to
  #    ensure we scope the tasks to the correct project.
  #
  #  `order` : hash<id, position> It also accepts a hash of object ID to
  #    position.
  def sort
    @tasks = Project.find(params[:project_id]).tasks

    ProjectTask.transaction do
      params[:order].each do |task_id, position|
        @tasks.find(task_id).update(position: position)
      end
    end

    render json: { status: 'success' }
  end
end

To improve this API, we could make it more reusable by abstracting the class type and order parameters.

I hope this gives a clearer walkthrough of adding drag-and-drop sorting with Stimulus and Shopify Draggable! Let me know if any part needs more explanation.