Hey. I’m only starting to learn HTMX and I decided to make a project to experiment how much of SPA functionality I can achieve with only HTMX/Alpine.js on my Django templates.
Part of the project is a drag-and-drop re-ordering of a list of objects, which on its HTMX part works fine and is successfully persisted to the db. Thing is, I can’t get the JS to work correctly, as only one drag and drop event happens, and the the functionality is permanently disabled until I refresh the page. Again, all the HTMX part (re-ordering of the list indexes and db alteration) works as intended.
I suspect the problem seems to be related to the instantiation of the Sortable class, but I’m out of my depth at that point. The htmx:afterSwap event never triggers (according to my debugging console.log), unlike the onEnd event which of course does trigger. But the thing is, I tried switching the htmx:afterSwap to an htmx:afterRequest, and it did trigger on the JS script, but nevertheless the drag and drop functionality remained disabled (until I reload the page in which case it works fine).
I know this is a bit off topic with regards to Django, but for the life of my I cannot figure out what is going wrong. Any help would be appreciated.
BTW link to the repository for full code is here: GitHub - nicoferreira90/reading_list_app
The head of the base.html file, which includes the Sortable.js script, which I copied and pasted from the HTMX page:
<head>
{% comment %} HTMX CDN {% endcomment %}
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq"
crossorigin="anonymous"></script>
{% comment %} Bootstrap CSS CDN {% endcomment %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
{% comment %} Bootstrap JS CDN {% endcomment %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
{% comment %} sortable.js CDN {% endcomment %}
<script src=" https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js "></script>
{% comment %} load Bootstrap theme {% endcomment %}
<link rel="stylesheet" href="{% static "bootstrap.min.css" %}">
<style>
h1, h2 {
text-align: center;
}
</style>
<script>
htmx.onLoad(function(content) {
var sortables = content.querySelectorAll(".sortable");
for (var i = 0; i < sortables.length; i++) {
var sortable = sortables[i];
var sortableInstance = new Sortable(sortable, {
animation: 150,
ghostClass: 'blue-background-class',
// Make the `.htmx-indicator` unsortable
filter: ".htmx-indicator",
onMove: function (evt) {
return evt.related.className.indexOf('htmx-indicator') === -1;
},
// Disable sorting on the `end` event
onEnd: function (evt) {
this.option("disabled", true);
console.log('onEnd has fired!')
}
});
// Re-enable sorting on the `htmx:afterSwap` event
sortable.addEventListener("htmx:afterSwap", function() {
console.log('afterSwap has fired!')
sortableInstance.option("disabled", false);
});
}
})
</script>
</head>
The reading_page which includes the htmx target, in this case the “book_list” div:
{% extends "base.html" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-7">
<h1 class="m-4">My Reading List</h1>
<div>
{% include "reading/partials/search.html" %}
</div>
<div class="d-flex justify-content-center align-items-center">
<div id="book_list" style="width: 70%;">
{% include "reading/partials/book_list.html" %}
</div>
</div>
</div>
<div class="col-5">
<h2 class="m-4 text-center">What do you want to read?</h2>
<div class="d-flex justify-content-center align-items-center">
<form action="{% url 'reading_page' %}" method="post" class="d-flex justify-content-center align-items-center" style="width: 90%;">
{% csrf_token %}
<input type="text" id="book-title" name="book-title" placeholder="title" class="form-control mr-2" required style="max-width: 250px; margin: 2px;">
<input type="text" id="book-author" name="book-author" placeholder="author" class="form-control mr-2" required style="max-width: 250px; margin: 2px;">
<button hx-post="{% url 'add_book' %}" hx-target="#book_list" type="submit" class="btn btn-success" style="margin: 2px;">
Add Book
</button>
</form>
</div>
<h1 class="m-4">Currently Reading...</h1>
{% include "reading/partials/currently_reading.html" %}
</div>
</div>
</div>
{% endblock content %}
And the book_list partial which has all the html code:
{% if book_list %}
<form class="sortable" hx-trigger="end" hx-post="{% url "book_sort" %}" hx-target="#book_list">
{% for book in book_list %}
<div>
<input type='hidden' name='book_order' value="{{ book.pk }}"/>
<div class="card bg-light mb-2" style="width: 100%;">
<div class="card-body">
<div class="row">
<!-- Text Section (Book Title and Author) -->
<div class="col-10">
<p class="h3"><b>{{book.title}}</b></p>
<p class="lead">{{book.author}}</p>
</div>
<!-- Button Section (Delete Button) -->
<div class="col-2 text-end">
<div class="row">
<h4><b>#{{book.order}}</b></h4>
</div>
<div class="row">
<button hx-delete="{% url 'delete_book' book.pk %}" hx-target="#book_list" hx-confirm="Are you sure you wish to delete this book?"
type="submit" class="btn btn-danger">
Delete
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</form>
{% else %}
<div class="card bg-light mb-3" style="width: 100%; padding: 2%;">
<h3>Your book list is empty.</h3>
</div>
{% endif %}