Multi-column drag and drop with SortableJS and Stimulus
Tags: stimulus, rails, webdev, javascript
Published: 2024-02-18 05:35:24

## Preface

Recently, in a Rails project, I was tasked with implementing the ability to drag and drop cards both within a single list and between two lists (or columns, for that matter). 

Without further ado, our choice was to use Stimulus. 

## Manual Approach

At first, I tried to implement drag and drop by reinventing the wheel, using [Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API#drag_events) directly for event handling. That is, I needed to write an action for all the events like "dragstart", "dragenter" and so on and so forth.

Quite quickly, it became clear that I was doing absolutely useless job and only complicated code support: describing every step of the drag-and-drop process was quite a pain. There was code that was not very easy to understand at first (especially the calculation of where to throw the cards, and not only that).

## Stimulus Sortable Component

Well, the obvious thought was to look for something Stimulus-related and ready to use, and there was a solution: [Stimulus Sortable](https://www.stimulus-components.com/docs/stimulus-sortable).

Well, it worked. But only for one case: dragging stuff within only one column. You see, stimulus-sortable uses [SortableJS](https://github.com/SortableJS/Sortable) under the hood, which is powerful. 

But it turned out, that the Stimulus component only handled the `onUpdate` event and wrapped only a few options out of many available in the original SortableJS. That was not enough for multi-column drag and drop support.

Well, it's open source, then why not enhance it? And I did locally, it was not that hard: pulled the [stimulus-sortable repository](https://github.com/stimulus-components/stimulus-sortable), wrote a couple lines of code (see diff below), ran `yarn build` and provided the path to my local package in my original project. It was good to go and it worked.

```diff
// src/index.ts 

export default class extends Controller {
  // ...
+ groupValue: string

// ...

static values = {
    // ...
+   group: String
// ... 
get options (): Sortable.Options {
    return {
      // ...
+     onChange: this.defaultOptions.onChange,
+     group: this.groupValue || this.defaultOptions.group || undefined,
    }
  }
```

All you need to handle multi-column drag in SortableJS is to [set the same `group` option value](https://sortablejs.github.io/Sortable/#shared-lists) for every list/column

That was a solution, but... I have my own copy of the package now. That comes with a cost of maintaining my own fork, and we didn't want that.

## Wait a minute, can't I just ...

_...use SortableJS directly in my regular Stimulus controller without having an odd dependency_? Well, yes you can, and it's actually easier, than using the Stimulus component wrapper around it.

Just do the: 

```
yarn add sortablejs
```
in your project and write this in your Stimulus controller:

```js
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs";

export default class extends Controller {
  static values = {
    group: String
  }

  connect() {
    new Sortable(this.element, {
      group: this.groupValue,
      onChange: this.onChange
    })
  }

  onChange(event) {
    // ... here goes your logic to send AJAX requests with updated data
  }
}
```

Don't forget to register your controller in `index.js`:

```js
import DragController from "./drag_controller"
application.register("drag", DragController)
```

Don't forget to add `data-controller` for your parent container and provide the shared group name:

```html
<div class="container" data-controller="drag">
  <div class="left-column" data-drag-group-value="your_group_name">
    <!-- cards go here -->
  </div>

  <div class="right-column" data-drag-group-value="your_group_name">
    <!-- and cards go here -->
  </div>
</div>
```

Yeah, that's it. Actually, there was no need for a wrapper at all. You can now drag and drop the cards within the same list as well as between multiple lists, just handle the `onChange` event for stuff like updating position values.

And if you need more features that are available in SortableJS, you can just implement them the same way