Django Update Multiple Records with Selected Checkbox Using HTMX

Adi Ramadhan
5 min readNov 20, 2023

References:
django, bootstrap, htmx.

Update Multiple Records

Step 1: Preparation, Create Django Project, Inital Migration
create virtualenv: virtualenv venv
start virtualenv: venv/Scripts/activate
install Django in virtualenv: pip install django==4.2
Create Django: django-admin startproject myproject
Go to myproject folder: cd myproject
Initial Migration: python manage.py migrate

Step 2: Create Django Apps
Create apps: python manage.py startapp myapp

Step 3: Project Setting: Register Apps, Set Templates Folder (myproject/settings.py)

...

INSTALLED_APPS = [
...
'myapp', #updated
]

...

TEMPLATES = [
...
'DIRS': [Path(BASE_DIR, 'templates')], #updated
...
]


...

Step 4: Create Models (myapp/models.py), Makemigration and Migrate

from django.db import models

class Book(models.Model):

title = models.CharField(db_column='title', max_length=100, blank=False)
status = models.CharField(db_column='status', max_length=50, blank=False)
author = models.CharField(db_column='author', max_length=100, blank=False, null=False)
year = models.IntegerField(db_column='year',blank=False, default=0)

class Meta:
db_table = 'book'
verbose_name = 'Book'
verbose_name_plural = 'Books'
def __unicode__(self):
return self.title
def __str__(self):
return self.title

Make migrations: python manage.py makemigrations
Migrate: python manage.py migrate

Step 5: Create Forms myapp/forms.py

from django import forms


STATUS_CHOICES =(
("AVAILABLE", "AVAILABLE"),
("NOT AVAILABLE", "NOT AVAILABLE")
)

class BookForm(forms.Form):

title = forms.CharField(label = "Title", widget = forms.TextInput( attrs={'class': 'form-control'} ))
author = forms.CharField(label = "Author", widget = forms.TextInput( attrs={'class': 'form-control'} ))
status = forms.ChoiceField(label = "Status", choices=STATUS_CHOICES, widget = forms.Select( attrs={'class': 'form-control'} ))
year = forms.CharField(label = "Year", widget = forms.NumberInput( attrs={'class': 'form-control'} ))

Step 6: Create HTML Files in Templates Folder

Create templates folder in main app

Create templates/index.html

<!DOCTYPE html>
<html lang="en">

<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" />

<title>Books</title>
</head>

<body>
<div class="container">
<div class="px-5 my-5 text-center">
<h1>Books</h1>
</div>

<button hx-get="{% url 'book_add' %}" hx-target="#dialog" class="btn btn-primary mb-2">
Add a book
</button>

<!-- load booklist with HTMX -->
<div hx-trigger="load, bookListChanged from:body" hx-get="{% url 'book_list' %}" hx-target="this">
<span >Loading...</span>
</div>


</div>

<!-- Placeholder for the modal -->
<div id="modal" class="modal fade">
<div id="dialog" class="modal-dialog" hx-target="this"></div>
</div>

<!-- Empty toast to show the message -->
<div class="toast-container position-fixed top-0 end-0 p-3">
<div id="toast" class="toast align-items-center text-white bg-success border-0" role="alert" aria-live="assertive"
aria-atomic="true">
<div class="d-flex">
<div id="toast-body" class="toast-body"></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
</div>
</div>


<!-- Scripts: Bootstraps, HTMX, and custom JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/htmx.org@1.6.1/dist/htmx.min.js"></script>

<script>
; (function () {
const modal = new bootstrap.Modal(document.getElementById("modal"))

htmx.on("htmx:afterSwap", (e) => {
// Response targeting #dialog => show the modal
if (e.detail.target.id == "dialog") {
modal.show()
}
})

htmx.on("htmx:beforeSwap", (e) => {
// Empty response targeting #dialog => hide the modal
if (e.detail.target.id == "dialog" && !e.detail.xhr.response) {
modal.hide()
e.detail.shouldSwap = false
}
})

// Remove dialog content after hiding
htmx.on("hidden.bs.modal", () => {
document.getElementById("dialog").innerHTML = ""
})
})()
</script>
<script>
; (function () {
const toastElement = document.getElementById("toast")
const toastBody = document.getElementById("toast-body")
const toast = new bootstrap.Toast(toastElement, { delay: 2000 })

htmx.on("showMessage", (e) => {
toastBody.innerText = e.detail.value
toast.show()
})
})()
</script>

</body>

</html>

Create templates/book_list.html

<div class="col" hx-include="#checked-books">
<button class="btn btn-primary" hx-post="{% url 'book_delete_selected'%}"
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>Delete Selected</button>
<button class="btn btn-primary" hx-post="{% url 'book_available_selected'%}"
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>Set AVAILABLE for Selected</button>
<button class="btn btn-primary" hx-post="{% url 'book_notavailable_selected'%}"
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>Set NOT AVAILABLE for Selected</button>
</div>

<form id="checked-books">
<table class="table">
<thead>
<tr>
<th>
<input type="checkbox" class="form-check-input" id="selectAll" />
</th>
<th>Title</th>
<th>Author</th>
<th>Status</th>
<th>Year</th>

</tr>
</thead>
<tbody>


{% for book in books %}
<tr id="book-{{ book.pk }}" class="book-row">
<td>
<input type='checkbox' class="form-check-input" name='{{ book.pk }}' value='{{ book.title }}'>
</td>
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>{{ book.status }}</td>
<td>{{ book.year }}</td>

</tr>
{% endfor %}

</tbody>

</table>
</form>




<script>

$('#selectAll').click(function (e) {
if (this.checked) {
// Iterate each checkbox
$(':checkbox').each(function () {
this.checked = true;
});
} else {
$(':checkbox').each(function () {
this.checked = false;
});
}
});

</script>

Create templates/book_form.html

<form hx-post="{{ request.path }}" hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}' class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Book Form</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>

<div class="modal-body">

{% for field in form %}
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{{ field }}
<div class="invalid-feedback">{{ field.errors|first }}</div>
{% endfor %}

</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
html files in template

Step 7: Create Function Views myapp/views.py

from django.shortcuts import render
from .forms import BookForm
from .models import Book
import json
from django.shortcuts import HttpResponse

def index(request):
return render(request, 'index.html', {})

def book_list(request):
books = Book.objects.all()
return render(request, 'book_list.html', {'books': books})

def book_add(request):
if request.method == "POST":
form = BookForm(request.POST)
if form.is_valid():
book = Book.objects.create(
title = form.cleaned_data.get('title'),
author = form.cleaned_data.get('author'),
status = form.cleaned_data.get('status'),
year = form.cleaned_data.get('year')
)
return HttpResponse(
status=204,
headers={
'HX-Trigger': json.dumps({
"bookListChanged": None,
"showMessage": f"{book.title} added."
})
})
else:
return render(request, 'book_form.html', {
'form': form,
})
else:
form = BookForm()
return render(request, 'book_form.html', {
'form': form,
})

def book_delete_selected(request):

for id, title in request.POST.items():
#delete action
book = Book.objects.filter( pk = id).first()
if (book):
book.delete()

return HttpResponse(
status=204,
headers={
'HX-Trigger': json.dumps({
"bookListChanged": None,
"showMessage": f"Selected book is deleted"
})
})

def book_available_selected(request):

for id, title in request.POST.items():
#update action
book = Book.objects.filter( pk = id).first()
if (book):
book.status = "AVAILABLE"
book.save()

return HttpResponse(
status=204,
headers={
'HX-Trigger': json.dumps({
"bookListChanged": None,
"showMessage": f"Selected book is updated to AVAILABLE"
})
})

def book_notavailable_selected(request):

for id, title in request.POST.items():
#update action
book = Book.objects.filter( pk = id).first()
if (book):
book.status = "NOT AVAILABLE"
book.save()

return HttpResponse(
status=204,
headers={
'HX-Trigger': json.dumps({
"bookListChanged": None,
"showMessage": f"Selected book is updated to NOT AVAILABLE"
})
})

Step 8: Setup URLS

Create myapp/urls.py


from django.urls import path
from . import views

urlpatterns = [

path('', views.index, name="index"),
path('book/add', views.book_add, name="book_add"),
path('book/list', views.book_list, name="book_list"),

path('book/delete', views.book_delete_selected, name="book_delete_selected"),
path('book/available', views.book_available_selected, name="book_available_selected"),
path('book/notavailable', views.book_notavailable_selected, name="book_notavailable_selected"),

]

Update myproject/urls.py

from django.contrib import admin
from django.urls import path, include #updated

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('myapp.urls')), #updated
]

Step 9: Run Server and Testing
Run Server: python manage.py runserver
Testing: http://127.0.0.1:8000/

Add book
Update multiple records

--

--