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 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.
Well, it worked. But only for one case: dragging stuff within only one column. You see, stimulus-sortable uses SortableJS 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, 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.
// 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 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:
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
:
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:
<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
Top comments (1)
Some time ago, I needed this too, and added issue on the
stimulus-sortable
repo.Sadly, activity of the project creator is limited (which is fully understandable), so I found some workaroud myself (shown in issue).
Thanks for this article, I will soon to use this functionality again in slightly different context, so it will come handy.
Maybe you can create a PR in the project (or first ask in the issue if that would be acceptable for the maintainer)? It would be really nice to have that support in the component, making it acessible for many devs.