--sortable
option
Super Scaffolding with the Note: The sortable controller and these docs have recently changed.
These instructions are for the new `sortable_controller.js` which has removed the dependency on `dragula` and which works slightly differently.
You can find the old documentation for the `dragula` based controller here.
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. On the button, add a data-action
.
For instance:
<div data-controller="sortable-wrapper">
<table>...</table>
<button data-action="sortable-wrapper#saveSortOrder">Save Sort Order</button>
</div>
/* 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"))
}
}
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 native drag and drop event handling as provided by modern browsers.
The following events are dispatched when significant events related to the list occur. (Note these are new events that were not previously emitted by the dragula
based controller.)
Event name | Fired when |
---|---|
sortable:start | a drag via drag handle is started |
sortable:reordered | the items in the list are reorderd during a drag |
sortable:end | a sortable item is released |
sortable:saved | the new order has been persisted after a drop |
These are events that we'll emit to retain some backwards compatibility with the old dragula
based controller. Be aware that they may not behave exactly the same. In general you should prefer the events above.
Event name | Fired when | Better option |
---|---|---|
sortable:drag | a drag via drag handle is started | sortable:start |
sortable:dragend | a sortable item is released | sortable:end |
sortable:drop | a sortable item is released | sortable:end |
sortable:shadow | the items in the list are reorderd during a drag | sortable:reordered |
Note: The old dragula
based controller used to emit a few events that were particular to dragula
. The new controller does not emit the following events:
event name |
---|
sortable:cancel |
sortable:remove |
sortable:over |
sortable:out |
sortable:cloned |
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?
Add a data-controller
attribute to the <table>
tag that wraps the sortable <tbody>
:
<table data-controller="confirm-reorder">
/* 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:end
, sortable:start
(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:end->confirm-reorder#requestConfirmation sortable:start->confirm-reorder#prepareForRevertOnCancel save-sort-order->sortable#saveSortOrder"
data-confirm-reorder-target="sortable"
...
>
Drag handles
With the release of the new sortable_controller
new super scaffolds will include a cell at the begining of each row with an icon as a drag handle.
For pre-existing super scaffolds the new controller will detect that the handles are missing and it will programatically add them to the table.
If you want more control over the handles you should update your templates to include handles, which will prevent them from being automatically added.
Using the example of a sortable Page
model from above you'll want to update two files.
app/views/account/pages/_page.html.erb
In app/views/account/pages/_page.html.erb
you should add a class="group"
attribute to the <tr>
, and add a <td>
containing a drag handle as the first cell inside the <tr>
. Be sure to include data-sortable-target="handle"
on the cell so that the controller recognizes it as a handle.
<tr data-id="<%= page.id %>" class="group"> <!-- Add the class attribute on this line! -->
<td class="cursor-grab active:cursor-grabbing" data-sortable-target="handle"><i class="ti ti-menu opacity-25 group-hover:opacity-100"></i></td> <!-- Add this line! -->
<!-- Your existing cells here -->
</tr>
app/views/account/pages/_index.html.erb
In app/views/account/pages/_index.html.erb
you should add an empty <td>
as the first cell within the <tr>
in the <thead>
. You probably want to set a width for that cell to make things look nice.
<thead>
<tr>
<th class="w-6"></th> <!-- Add this line! -->
<!-- Your existing cells here -->
</tr>
</thead>
Drag styling
The visual styling of dragged items and drop targets is handled by the addition and removal of specific class names by the controller.
To override the default styles you could write some custom CSS like this:
.sortable-active-dropzone {
/* Styles for the entire table when a drag is in progress. */
}
.sortable-active-item {
/* Styles for the item that is being dragged. */
}
.sortable-drop-target {
/* Styles for the empty space representing the new location for the item being dragged. */
}
Any styles you add to those classes will be combined with the defaults.
If you'd like to start fresh with a clean set of classes you can tell the controller to use different class names by adding some data attributes to the primary sortable
target (the <tbody>
tag by default).
For instance:
<tbody data-controller="sortable" data-sortable-reorder-path-value="<%= url_for [:reorder, :account, context, collection] %>"
<%# Use Stimulus class attributes to make the controller apply different class names: %>
data-sortable-active-dropzone-class="your-custom-active-dropzone-class-name"
data-sortable-active-item-class="your-custom-active-item-class-name"
data-sortable-drop-target-class="your-custom-drop-target-class-name"
>