--sortable
option
Super Scaffolding with the When issuing a rails generate super_scaffold
command, you can pass the --sortable
option like this:
# E.g. Pages belong to a Site and are sortable via drag-and-drop:
rails generate super_scaffold Page Site,Team name:text_field path:text_area --sortable
The --sortable
option:
- Wraps the table's body in a
sortable
Stimulus controller, providing drag-and-drop re-ordering; - Adds a
reorder
action to your resource viainclude SortableActions
, triggered automatically on re-order; - Adds a
sort_order
attribute to your model to store the ordering; - Adds a
default_scope
which orders bysort_order
and auto incrementssort_order
on create viainclude Sortable
on the model.
Disabling Saving on Re-order
By default, a call to save the new sort_order
is triggered automatically on re-order.
To disable auto-saving
Add the data-sortable-save-on-reorder-value="false"
param on the sortable
root element:
<tbody data-controller="sortable"
data-sortable-save-on-reorder-value="false"
...
>
To manually fire the save action via a button
Since the button won't be part of the sortable
root element's descendants (all its direct descendants are sortable by default), you'll need to wrap both the sortable
element and the save button in a new Stimulus controlled ancestor element.
/* sortable-wrapper_controller.js */
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "sortable" ]
saveSortOrder() {
if (!this.hasSortableTarget) { return }
this.sortableTarget.dispatchEvent(new CustomEvent("save-sort-order"))
}
}
On the button, add a data-action
<button data-action="sortable-wrapper#saveSortOrder">Save Sort Order</button>
And on the sortable
element, catch the save-sort-order
event and define it as the sortable
target for the sortable-wrapper
controller:
<tbody data-controller="sortable"
data-sortable-save-on-reorder-value="false"
data-action="save-sort-order->sortable#saveSortOrder"
data-sortable-wrapper-target="sortable"
...
>
Events
Under the hood, the sortable
Stimulus controller uses the dragula library.
All of the events that dragula
defines are re-dispatched as native DOM events. The native DOM event name is prefixed with sortable:
dragula event name | DOM event name |
---|---|
drag | sortable:drag |
dragend | sortable:dragend |
drop | sortable:drop |
cancel | sortable:cancel |
remove | sortable:remove |
shadow | sortable:shadow |
over | sortable:over |
out | sortable:out |
cloned | sortable:cloned |
The original event's listener arguments are passed to the native DOM event as a simple numbered Array under event.detail.args
. See dragula's list of events for the listener arguments.
drop
Event
Example: Asking for Confirmation on the Let's say we'd like to ask the user to confirm before saving the new sort order:
Are you sure you want to place DROPPED ITEM before SIBLING ITEM?
/* confirm-reorder_controller.js */
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "sortable" ]
requestConfirmation(event) {
const [el, target, source, sibling] = event.detail?.args
// sibling will be undefined if dropped in last position, taking a shortcut here
const areYouSure = `Are you sure you want to place ${el.dataset.name} before ${sibling.dataset.name}?`
// let's suppose each <tr> in sortable has a data-name attribute
if (confirm(areYouSure)) {
this.sortableTarget.dispatchEvent(new CustomEvent('save-sort-order'))
} else {
this.revertToOriginalOrder()
}
}
prepareForRevertOnCancel(event) {
// we're assuming we can swap out the HTML safely
this.originalSortableHTML = this.sortableTarget.innerHTML
}
revertToOriginalOrder() {
if (this.originalSortableHTML === undefined) { return }
this.sortableTarget.innerHTML = this.originalSortableHTML
this.originalSortableHTML = undefined
}
}
And on the sortable
element, catch the sortable:drop
, sortable:drag
(for catching when dragging starts) and save-sort-order
events. Also define it as the sortable
target for the confirm-reorder
controller:
<tbody data-controller="sortable"
data-sortable-save-on-reorder-value="false"
data-action="sortable:drop->confirm-reorder#requestConfirmation sortable:drag->confirm-reorder#prepareForRevertOnCancel save-sort-order->sortable#saveSortOrder"
data-confirm-reorder-target="sortable"
...
>