Sortable Stimulus Controller
Stimulus controller
if (this.hasItemTarget) { (1)
this.itemTargets.forEach(item => {
item.setAttribute('style', 'z-index: 1000;') (2)
item.classList.add('draggable-source') (3)
})
...
}
1 | it is always better to check for the existence of any target before make use of it. |
2 | make sure our element is always on top to be draggable |
3 | set a class for draggable, default is '.draggable-source' |
Initialize Sortable
const sortable = new Sortable(this.element, { (1)
draggable: '.draggable-source', (2)
handle: this.handleValue, (3)
distance: 5, (4)
mirror: {
constrainDimensions: true (5)
}
})
1 | this.element : the <div> element with data-controller="sortable" |
2 | which elements should be draggable. Here: our Bootstrap v5 list group inner item |
3 | optional: set a handle for draggable, may be null |
4 | how much pixel the item should be moved before draggable takes action |
5 | don’t change the size of our draggable element during the move |
Event sortable:start
sortable.on('sortable:start', function(event) {
let item = event.dragEvent.source
item.setAttribute('style', 'z-index: 1000; background-color: #FFFFAB;') (1)
})
1 | ensure our dragged item is always on top and colorize it. |
Event sortable:stop
sortable.on('sortable:stop', function(event) {
let item = event.dragEvent.source (1)
let item_id = item.id.substr(item.id.lastIndexOf("_")+1) (2)
let item_type = item.id.substr(0, item.id.lastIndexOf("_")) (3)
let url = item.getAttribute('data-url') (4)
let data = {[item_type]: { position: (event.data.newIndex + 1) }} (5)
let token = document.head.querySelector('meta[name="csrf-token"]').getAttribute('content') (6)
fetch(url, {
method: 'PUT',
credentials: 'same-origin',
headers: {
"X-CSRF-Token": token, (6)
"Content-type": "application/json", (7)
"Accept": "text/vnd.turbo-stream.html" (8)
},
body: JSON.stringify(data)
})
.then(r => r.text())
.then(html => Turbo.renderStreamMessage(html)) (9)
})
1 | the dragged item itself |
2 | extract the numeric id from dom_id(item), i.e. task_123 → '123' |
3 | item type, i.e. task_123 → 'task' |
4 | url for update via PUT |
5 | build the parameter hash: with the standard controller update method we need something like {"task": { "position": <new position>}} |
6 | to overcome CSRF protection, fetch the token from the header and use it in the PUT/PATCH request. |
7 | we are sending JSON data … |
8 | … but we want a turbo stream response |
9 | finally render the received turbo stream message |
if you like a more compact ajax call, have a look at https://github.com/rails/requestjs-rails. It includes some steps as automatically use the CSRF token, but it comes with the cost of an additional dependency. |
The complete controller
import { Controller } from "@hotwired/stimulus"
import { Sortable } from '@shopify/draggable';
export default class extends Controller {
static targets = ['item']
static values = {
handle: String
}
connect() {
let _this = this
if (this.hasItemTarget) {
this.itemTargets.forEach(item => {
item.setAttribute('style', 'z-index: 1000;')
item.classList.add('draggable-source')
})
const sortable = new Sortable(this.element, {
draggable: '.draggable-source',
handle: this.handleValue,
distance: 15,
mirror: {
constrainDimensions: true
}
})
sortable.on('sortable:start', function(event) {
let item = event.dragEvent.source
item.setAttribute('style', 'z-index: 1000; background-color: #FFFFAB;')
})
sortable.on('sortable:stop', function(event) {
// avoid update if no changes detected
if (event.oldIndex == event.newIndex) {
return
}
let item = event.dragEvent.source
// needs something like id="<%= dom_id item %>"
let item_id = item.id.substr(item.id.lastIndexOf("_")+1)
let item_type = item.id.substr(0, item.id.lastIndexOf("_"))
let url = item.getAttribute('data-url')
let data = {[item_type]: { position: (event.data.newIndex + 1) }}
let token = document.head.querySelector('meta[name="csrf-token"]').getAttribute('content')
fetch(url, {
method: 'PUT',
credentials: 'same-origin',
headers: {
"X-CSRF-Token": token,
"Accept": "text/vnd.turbo-stream.html",
"Content-type": "application/json"
},
body: JSON.stringify(data)
})
.then(r => r.text())
.then(html => Turbo.renderStreamMessage(html))
})
}
}
}