#
Drag & Drop with Sorting
#
Implementing Drag-and-Drop Sorting with htmx and ASP.NET Core
The Drag-and-Drop Sortable pattern demonstrates how to integrate a specialized JavaScript library like Sortable.js with htmx to create a reorderable list that syncs its state with the server automatically. This provides a highly intuitive UI for managing sequences or priorities.
#
1. The Frontend: Sortable.js & htmx
In Index.cshtml, we wrap our list in a form. We use a small Hyperscript block to initialize the Sortable.js library and bridge its events over to htmx.
Index.cshtml
<form id="sortable-form"
hx-post="?handler=Reorder"
hx-trigger="end"
hx-target="#sortable-list"
hx-indicator="#reorder-spinner">
@Html.AntiForgeryToken()
<div id="sortable-list" class="list-group"
_="on load or htmx:afterSwap from #sortable-form
js(me)
if (me._sortable) me._sortable.destroy();
me._sortable = Sortable.create(me, {
animation: 150,
handle: '.handle',
onEnd: function() {
htmx.trigger('#sortable-form', 'end');
}
});
end">
<partial name="_ItemList" model="Model.Items" />
</div>
</form>
Key htmx and Hyperscript attributes:
hx-post="?handler=Reorder": Specifies the server endpoint to receive the new order.hx-trigger="end": htmx waits for a custom DOM event namedendto be fired before sending the request.on load or htmx:afterSwap: Hyperscript ensures that Sortable.js is initialized (or re-initialized after a swap) on the list element.onEnd: function() { htmx.trigger(...) }: Inside the Sortable.js configuration, we manually trigger theendevent that htmx is listening for whenever a drag operation finishes.
#
2. The Item List Partial
The list items contain a hidden input. When Sortable.js reorders the DOM elements, these inputs move with them. When htmx submits the form, it sends these IDs in their new order.
_ItemList.cshtml
@model List<Item>
@foreach (var item in Model)
{
<div class="list-group-item d-flex align-items-center">
<input type="hidden" name="itemIds" value="@item.Id" />
<i class="fas fa-grip-vertical handle"></i>
<span>@item.Name</span>
<span class="ml-auto">Order: @item.Order</span>
</div>
}
#
3. The Backend: C# PageModel
The server receives an array of integers representing the IDs in the order they appear in the DOM. It updates the database (or in-memory store) accordingly and returns the updated list fragment.
Index.cshtml.cs
public class Index : PageModel
{
public List<Item> Items { get; set; } = new();
// Handler for reordering items
public IActionResult OnPostReorder(int[] itemIds)
{
if (itemIds != null)
{
for (int i = 0; i < itemIds.Length; i++)
{
var id = itemIds[i];
var item = _db.Items.FirstOrDefault(x => x.Id == id);
if (item != null)
{
item.Order = i + 1; // Update sequence based on array index
}
}
}
Items = _db.Items.OrderBy(i => i.Order).ToList();
// Return only the partial view to update the UI with new order numbers
return Partial("_ItemList", Items);
}
}
#
Why this works well
- Best of Both Worlds: You use a mature, specialized library (Sortable.js) for complex touch/drag interactions, but keep the data synchronization logic in htmx.
- No Manual JSON Mapping: Because Sortable.js rearranges the actual
<input>elements in the DOM, htmx's standard form submission naturally sends the correct sequence without any custom data mapping. - Visual Feedback: By targeting the list body and returning a partial, you can update "Order" badges or other sequence-dependent UI elements immediately after the drop.
- Resilient Lifecycle: Using Hyperscript's
htmx:afterSwaptrigger ensures the drag-and-drop functionality continues working even after the list has been updated and replaced by htmx.