Django + HTMX CRUD application

Django + HTMX CRUD application

Creating a basic CRUD application with Django and HTMX

Introduction

Gone are the days of writing Ajax requests with javascript, just add a few parameters to the HTML content tags and you will be ready for sending requests to your backend. So, we are going back in time and correcting the way we think of APIs and client/server-side rendering. We are referring to the Hypermedia model for levering the server-side processing of data. Let's get our feets wet with this ancient but revolutionary methodology of development with HTMX.

Yes, HTMX can be used for the API/server-side calls directly in the HTML. We will be exploring the basis of HTMX by creating a basic CRUD application.

What is HTMX?

The first question that might come up is what and why HTMX? Htmx is a great library, it's a javascript library but wait. It is a javascript library designed to allow us to write less or no javascript at all. It acts as a way to send AJAX requests without you writing any javascript. It uses native browser features directly from HTML.

So, we can use HTMX to create interactive templates in our Django application. We can dynamically call and fetch data from the server by using simple HTML attributes like hx-get, hx-post, etc. We'll cover those in this article.

You can check the source code used in this article on this GitHub repository.

Setup Django Project

We'll be creating a Django project from scratch and designing a basic blog kind of app. We will be creating a quite simple project with a couple of apps like user for authentication and article for the CRUD part of our blog application.

To set up a django project, we can run the following commands to quickly get up and running with a base django project.

mkdir htmx_blog
python3 -m venv .venv
source .venv/bin/activate
pip install django
django-admin startproject htmx_blog .

I have a base user model that I use for a simple authentication system in some basic django projects, you can define your own user app or get the app from here.

So, that being said, we will be using the user model for the article model which we will be defined next. By creating a basic signup functionality, you are good to go!

Create the Article app

We will need at least an app to work with htmx as we will define models, views, and URLs later as we configure the htmx.

django-admin startapp article

After the app has been created, you can add those app labels into the INSTALLED_APPS config in the settings.py file. The user app and the article app need to be added to the installed apps for the django to pick those up for various contexts related to the project.

# htmx_blog/settings.py

INSTALLED_APPS = [
    ...
    ...
    ...

    'article',  
    'user',
]

We are sone with the base setup, we also would require a few more configs for the proper working of the project.

Setup Templates and Static files

Templates will play an important role in the htmx part, so it is equally important to configure them properly before dabbling into the htmx and client-side rendering of data.

I like to keep all the templates in a single folder in the BASE_DIR with separate sub-folders for specific apps. Also a single static folder with css, js, and images as the sub-folfers for a larger project.

mkdir templates static

Further, configure the created static and templates in the settings.


TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, "templates")],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

STATIC_URL = 'static/'
STATICFILES_DIRS = [str(BASE_DIR/ "static")]
STATIC_ROOT = BASE_DIR / "staticfiles"

Initial migration

Run migration command for the user model and default model in the django project.

python manage.py makemigrations
python manage.py migrate

So, this project will also include authentication simple registration, and login/logout routes. We will be using the default Django User model by creating an abstract user just in case we require any additional attributes.

Setup HTMX

We don't have to configure much for using HTMX as it is a javascript library, we can call it via a CDN or manually install it and link up the static javascript files. Either way, both are equally good, you may like the one I might like the other.

If you already have a base template, you can simply put the below script inside the head tag of the template. This will make us the htmx attributes available.

<script src="https://unpkg.com/htmx.org@1.8.0"></script>

If you don't have a base template, you can create one by creating an HTML file inside the templates directory. The name can be anything but be careful for following up as it might be different for me. I will choose base.html as the template for this project. It will look something like as follows:

<!-- tempaltes/base.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>HTMX Blog</title>
    {% load static %}
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <script src="https://unpkg.com/htmx.org@1.8.0"></script>
</head>
<body >
        <nav>
        <h2>HTMX Blog</h2>
        <div class="navbar">
          {% if user.is_authenticated %}
            <a class="nav-item nav-link" href="{% url 'logout' %}"><button class="btn btn-link">Logout</button></a>
          {% else %}
            <a class="nav-item nav-link" href="{% url 'login' %}"><button class="btn btn-link">Login</button></a>
            <a class="nav-item nav-link" href="{% url 'register' %}"><button class="btn btn-link">Register</button></a>
          {% endif %}
        </div>
        </nav>

    {% block body %}
    {% endblock %}
</body>
</html>

I have a nav bar with my user authentication views, simply a login or signup button if the user is not logged in and a log-out button if the user is authenticated. We have added the htmx script file from the CDN just before the end of the head tag. We also have included the bootstrap CSS file for a decent UI which we will be creating in this post.

That is one of the ways, htmx can be injected into an HTML template, you can even download the javascript file from the htmx cdn. Further, this can be downloaded or pasted into your local folder and served as a static file or embedded directly into an HTML template.

Defining Models

We will start the tutorial by defining the model of the application we are creating. Here, we will create a simple Article model with a few parameters like title, content, author, etc.

from django.db import models
from user.models import Profile

class Article(models.Model):
    Article_Status = (
        ("DRAFT", "Draft"),
        ("PUBLISHED", "Published"),
    )
    title = models.CharField(max_length=128, unique=True)
    content = models.TextField()
    author = models.ForeignKey(Profile, on_delete=models.CASCADE)
    status = models.CharField(
        max_length=16,
        choices=Article_Status,
        default=Article_Status[0],
    )

    def __str__(self):
        return self.title

In the above model Article, we have a few fields like title simple Character Field, content as a text field as it will be a large text as the post body, author which is a ForeignKey to the User Model. We also have the status, which is defined as a character field but with a few choices like draft or published, we can further modify this status as public or private. But just keeping it simple and easy to understand.

The object reference name for this model is the title as we have defined in the dunder string method. So, that is a simple model created, we can now migrate the changes into the database for adding the tables and attributes.

python manage.py makemigrations
python manage.py migrate

This will make migrations to the database i.e. convert the python model class into database tables and attributes. So, once the migration process is completed successfully, we can move into the crust of this article which is to actually design the views. In the next section, we will be utilizing the models in our views for representing the data on the templates.

Creating Article Form

Before diving into the views section, we need a few things like the Article Form, which will be a Django Model-based form. It will help us a lot in creating or updating the fields for the article model. We can define a form in a python file called forms.py, it's not necessary to keep your forms in the forms.py but if you have a lot of forms and models, it becomes a good practice to organize the components of our app. So, I'll be creating a new file inside of the article app called forms.py and defining the ArticleForm.

# article/forms.py

from django import forms
from .models import Article


class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        exclude = (
            "created",
            "updated",
            "author",
        )
        widgets = {
            "title": forms.TextInput(
                attrs={
                    "class": "form-control",
                    "style": "max-width: 450px; align: center;",
                    "placeholder": "Title",
                }
            ),
            "content": forms.Textarea(
                attrs={
                    "class": "form-control",
                    "style": "max-width: 900px;",
                    "placeholder": "Content",
                }
            ),
        }

So, the forms are inherited from the [ModelForm] which allows us to create forms based on our model. So, we specify the model name which in our case is Article and further we can have exclude or fields tuples. To exclude certain fields in the actual form, just parse the tuple of those attributes and if you want to only select a few attributes, you can specify the fields tuple and mention the required fields for the form.

So, if we have a lot of things to be included in the form, we can specify only the attributes to be excluded with the exclude tuple. And if we have a lot of fields to be excluded, we can use the fields tuple to specify which attributes to use in the form.

Let's take an example: For the above ArticleForm, if we wanted to specify the required fields to be included in the form, then we might use the fields tuple like below the rest will be not rendered in the form fields.

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = (
            "title",
            "content",
            "status",
        )

Both of them can be used, it just depends on how many fields you have to exclude or include in the rendered form.

We have also specified the widgets attribute which gives a bit more control on how we need to display the form in a template. So I have specified the type of input it needs to render like a simple text input for the title, text area for content, etc. The cool thing about this is it can automatically set these by knowing the type of field in the model, but sometimes it can be a bit undesired mostly with complex relationships and attributes.

Creating Views

Let's start creating views for creating, reading, updating, and deleting articles from the database. I will be using function-based views just because we are understanding the flow of how HTMX and Django can be integrated so we need to dive in deeper and understand the actual flow of the process.

Create View

So, creating articles seems like a good way to start off. We can create a simple function-based view which will initially load in an empty ArticleForm and if the request is GET we will render the form in the create.html template. If the request is POST which will be after we submit the form, we will validate the form and attach the current user as the author of the article and save the for instance which will create an article record and this object will be rendered to the detail template.

from django.shortcuts import render
from .models import Article
from .forms import ArticleForm

def createArticle(request):
    form = ArticleForm()
    context = {
        'form': form,
    }
    return render(request, 'articles/create.html', context)

Rendering the Form

We are creating an empty instance of ArticleForm and rendering it in the template. So, this will render the empty form in the create.html template.

<!-- templates/articles/create.html -->

{% extends 'base.html' %}

{% block body %}
<div hx-target="this" hx-swap="outerHTML">
  <form>
    {% csrf_token %}
    {{ form.as_p }}
    <button hx-post="." class="btn btn-success"
      type="submit">Save</button>
  </form>
</div>
{% endblock %}

Now, here we are inheriting from the base template and creating a form tag in HTML with the {{ form}} for rendering the form fields, we finally have the button element for submitting the form. We have used the hx-post attribute. More on this in just a minute. So, this is we create a template for rendering the article form.

We have used the hx-post attribute here, which will send a POST request to the current URL represented by hx-post=".". You might have noticed the div attributes, the hx-target and hx-swap, so these are some of the many attributes provided by the htmx library for controlling the reactivity of the requests made. The hx-target allow us to specify the element or tag to which the data will be rendered. The hx-swap goes hand-in-hand for specifying the target DOM like innerHTML, outerHTML, etc. You can see the various options on the htmx docs. By specifying the hx-swap as outerHTML, we are saying to replace the entire element with the incoming content from the request which we will send with nearby request triggers.

We need to map the view to a URL in order to get a good idea about the request and parsed content.

We'll create a create/ route and bind it to the createArticle view with the name article-create.

# article/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('create/', views.createArticle, name='article-create'), 
]

This URL will be mapped to the global URL in the project, here we can simply specify the prefix for the URLs in the article app and include those URLs.

# htmx_blog/urls.py

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('user.urls'), name='auth'),
    path('', include('article.urls'), name='home'),
]

Feel, free to add any other URL pattern like for instance, the article app is at / i.e. 127.0.01.:8000/, you can add any other name like 127.0.0.1:8000/article/ by adding path('article/', include('article.urls')).

Django HTMX Create view Form Template

So, finally, we are sending a GET request to the 127.0.0.1:8000/create/ and this will output the form. As we have a POST request embedded in the button inside the form, we will send the POST request to the same URL -> 127.0.0.1:8000/create/.

Submitting the Form

Let's handle the POST request in the create view.

from django.shortcuts import render
from .models import Article
from .forms import ArticleForm

def createArticle(request):
    form = ArticleForm(request.POST or None)
    if request.method == 'POST':
        if form.is_valid():
            form.instance.author = request.user
            article = form.save()
            return render(request, 'articles/detail.html', {'article': article})
    context = {
        'form': form,
    }
    return render(request, 'articles/create.html', context)

Simple explanation

  • Create a form instance of ArticleForm with the request data or empty -> ArticleForm(request.POST or None)
  • If it's a POST request, validate and create the article, render the article object in detail.html template.
  • If it's a GET request, render the empty form in create.html

There are a few changes in the view, instead of initializing the form to empty i.e. ArticleForm(), we are initializing with ArticleForm(request.POST or None). This basically means that if we are having something in the request.POST dict, we will initialize the Form with that data or else an empty form instance.

Next, we check if the request if POST, if it is then we check if the form is valid i.e. the form fields are not empty or if any other constraint on the model attributes is satisfied or not. If the form data is valid, we attach the author as the currently logged-in User/user who sent the request. Finally, we save the form which in turn creates the article record in the database. We then render the created article in the detail.html template which is not yet created.

So, the htmx-post attribute has worked and it will send a post request to the same URL i.e. 127.0.0.1:8000/create and this will again trigger the view createArticle this time we will have request.POST data. So, we will validate and save the form.

Detail View

The detail view is used for viewing the details of an article. This will be rendered after the article has been created or updated. This is quite simple, we need an id or primary key(pk) of an article and render the title and content of the article in the template.

We pass in a primary key along with the request as a parameter to the view, the pk will be passed via the URL. We fetch the Article object with the id as the parsed pk and finally render the detail.html template with the article object. The context['article'] can be accessed from the template to render the specific attributes like title, content, etc.

# article/views.py

def detailArticle(request, pk):
    article = Article.objects.get(id=pk)
    context = {'article': article}
    return render(request, 'articles/detail.html', context)

We can now bind the view to a URL and parse the required parameter pk to the view.

from django.urls import path
from . import views

urlpatterns = [
    path('create/', views.createArticle, name='article-create'), 
    path('<int:pk>', views.detailArticle, name='article-detail'), 
]

We have parsed the pk as int to the URL parameter, so for an article with id=4, the URL will be, 127.0.0.1:8000/4/.

We need to create the template for rendering the context from the detailArticle view. So, we create the detail.html in the templates/articles folder. We inherit the base template and render the article.title and the article.content with a linebreaks template filter so as to display the content properly.

<!-- templates/articles/detail.html -->


{% extends 'base.html' %}
{% block body %}
<div id="article-card">
  <h2>{{ article.title }}
  <p>{{ article.content|linebreaks|safe }}</p>
<div>
{% endblock %}

Detail View Template

So, we can now use createArticle view as well as detailArticle view, this both are configured properly, so (CR) or CRUD is completed. We can add listArticle for listing out all the author's(logged-in user) articles.

List View

Listview of the articles is much similar to the detail view as it will return a list of articles rather than a single article.

So in the listArticle view, we will return all the articles with the author as the user who sent the request/logged-in user. We will parse this object list into the template as base.html or articles/list.html.

# article/views.py


def listArticle(request):
    articles = Article.objects.filter(author=request.user.id)
    context = {
        'articles': articles,
    }
    return render(request, 'base.html', context)

We will add the URL route for this as the / route that is on 127.0.0.1:8000/ this is the base URL for the article app and is the route for the listArticle view. So, we will display the list of articles on the homepage.

# article/urls.py


from django.urls import path
from . import views

urlpatterns = [
    path('<int:pk>', views.detailArticle, name='article-detail'), 
    path('create/', views.createArticle, name='article-create'), 
    path('', views.listArticle, name='article-list'), 
]

Let's create the template for the list view which will iterate over the articles and display the relevant data like the title and link to the article.

<!-- templates/articles/list.html -->

<ul id="article-list">
  {% for article in articles %}
  <li>
    <div class="card" style="width: 18rem;">
      <div class="card-body">
        <h5 class="card-title">{{ article.title }}</h5>
        <p class="card-text">{{ article.content|truncatewords:5  }}</p>
        <a href="{% url 'article-detail' article.id %}" class="card-link">Read more</a>
      </div>
    </div>
  </li>
  {% endfor %}
</ul>

We have used the truncatewords:5 template filter for only displaying the content of the articles till the first 5 words as it is just a list view, we don't want to display every detail of the article here.

List view Template

We can use this template to render in the base.html file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>HTMX Blog</title>
    {% load static %}
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <script src="https://unpkg.com/htmx.org@1.8.0"></script>
</head>
<body hx-target="this" hx-swap="outerHTML" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
        <nav>
        <h2>HTMX Blog</h2>
        <div class="navbar">
          {% if user.is_authenticated %}
            <a class="nav-item nav-link" href="{% url 'article-list' %}"><button class="btn btn-link">Home</button></a>
            <a class="nav-item nav-link" href="{% url 'logout' %}"><button class="btn btn-link">Logout</button></a>
          {% else %}
            <a class="nav-item nav-link" href="{% url 'login' %}"><button class="btn btn-link">Login</button></a>
            <a class="nav-item nav-link" href="{% url 'register' %}"><button class="btn btn-link">Register</button></a>
          {% endif %}
        </div>
        </nav>

    {% block body %}
    <a href="{% url 'article-create' %}"><button class="btn btn-success" >Create</button></a>
    {% include 'articles/list.html' %}
    {% endblock %}
</body>
</html>

We have now included the list.html template on the homepage and also added the create button as the link to the article-create URL.

Delete View

For deleting an article, we will simply rely on htmx for sending the request and on that request, we will delete the current article and render the updated list of articles.

With the deleteArticle view, we will take in two parameters the request which is by default for a Django function-based view, and the primary key as pk. Again we will parse the pk from the URL. We will delete the article object and get the latest list of articles. Finally, render the updated list of articles in the base template which is our list view.

# article/views.py


def deleteArticle(request, pk):
    Article.objects.get(id=pk).delete()
    articles = Article.objects.filter(author=request.user)
    context = {'article': articles}
    return render(request, "base.html", context)

We will add the deleteArticle into the URL patterns and call it article-delete with the URL of delete/<int:pk>. This will allow us to send a request to the URL 127.0.0.1:8000/delete/4 for deleting the article with id 4.

# article/urls.py


from django.urls import path
from . import views

urlpatterns = [
    path('', views.listArticle, name='article-list'), 
    path('<int:pk>', views.detailArticle, name='article-detail'), 
    path('create/', views.createArticle, name='article-create'), 
    path('delete/<int:pk>', views.deleteArticle, name='article-delete'), 
]

In the delete view, the template is important as we want to send a request appropriately to the defined URL. To do that, we will have a form but it won't have any inputs as such just a button that indicates to delete the current article. We will add the hx-delete attribute as the URL to the deleteArticle view. with the id of the article. This will send a request to the article-delete URL which will, in turn, trigger the view with the given id and delete the article.

We have added the hx-confirm attribute for showing a pop-up of confirmation of deleting the article. As you can see we have added a little script for adding csrf_token into the HTML, this is important in order to submit a form with a valid CSRFToken.

<!-- templates/article/delete.html -->

<script>
  document.body.addEventListener('htmx:configRequest', (event) => {
    event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
  })
</script>
<div >
  <form method="post" >
  {% csrf_token %}
    <button class="btn btn-danger"
      hx-delete="{% url 'article-delete' article.id %}"
      hx-confirm="Are you sure, You want to delete this article?"
      type="submit">
      Delete
    </button>
  </form>
</div>

Do you have a question like how do we access the article.id? we are not rendering the delete.html template from the view, so there is no context to pass. We will include this snippet into the detail view template, so as to have the option of deleting the current article.

We will modify the articles/detail.html template and include the delete.html template. This includes simply adding an HTML template in the specified location. So, we will basically inject the delete form into the detail template.

{% extends 'base.html' %}
{% block body %}
<div hx-target="this" hx-swap="outerHTML">
  <h2>{{ article.title }}
  {% include 'articles/delete.html' %}
  <p>{{ article.content|linebreaks|safe }}</p>
<div>
{% endblock %}

Hence, we will have a nice option to delete the article in the detail section, this can be placed anywhere but remember, we need to add the hx-target="this" and hx-swap="outerHTML" in the div so as to correctly swap the HTML content after the request has been made.

Update View

We can now move into the final piece of the CRUD i.e. Update. This will be similar to the createArticle with a couple of changes. We will parse parameters like pk to this view as well because we want to update a specific article. So, we will have to get the primary key of the article from the URL slug.

Inside the updateArticle view, we will first grab the article object from the parsed primary key. We will have two kinds of requests here, one will be for fetching the form with the current article data, and the next request will be the PUT request for actually saving the changes in the article.

The first request is simple as we need to parse the form data with the instance of the article object. We will call the ArticleForm with the instance of article this will load the data of the article into the form ready to render into the template. So once the GET request has been sent, we will render the template with the form pre-filled with the values of the article attributes.

# article/views.py


def updateArticle(request, pk):
    article = Article.objects.get(id=pk)
    form = ArticleForm(instance=article)
    context = {
        'form': form,
        'article': article,
    }
    return render(request, 'articles/update.html', context)

We will create a template in the templates/articles/ folder as update.html which will have a simple form for rendering the form fields and a button for sending a PUT request. We will render the form and then add a button element with the attribute hx-put for sending the PUT request to save changes to the article record. We will parse in the article.id for the primary key parameter to the view.

<!-- templates/articles/update.html -->

<div hx-target="this" hx-swap="outerHTML">
  <form>
    {% csrf_token %}
    {{ form.as_p }}
    <button hx-put="{% url 'article-update' article.id %}"
      type="submit">Update</button>
  </form>
</div>

We are yet to link the updateArticle into the URLs. We will add the view updateArticle into the URLs with the name as article-update and update/<int:pk as the slug pattern. This URL pattern will trigger the updateArticle when we send an HTTP request to the 127.0.0.1:8000/update/4 for updating the article with id as 4.

# article/urls.py


from django.urls import path
from . import views

urlpatterns = [
    path('', views.listArticle, name='article-list'), 
    path('<int:pk>', views.detailArticle, name='article-detail'), 
    path('create/', views.createArticle, name='article-create'), 
    path('delete/<int:pk>', views.deleteArticle, name='article-delete'), 
    path('update/<int:pk>', views.updateArticle, name='article-update'), 
]

This is not done yet, we will need to handle the PUT request as well i.e. when the form details have been modified and we are about to save changes to the form data. So, we will check for the request method's type. If it is a PUT request, we will have to process a few things.

# article/views.py


from django.http import QueryDict

def updateArticle(request, pk):
    article = Article.objects.get(id=pk)
    if request.method == 'PUT':
        qd = QueryDict(request.body)
        form = ArticleForm(instance=article, data=qd)
        if form.is_valid():
            article = form.save()
            return render(request, 'articles/detail.html', {'article': article})
    form = ArticleForm(instance=article)
    context = {
        'form': form,
        'article': article,
    }
    return render(request, 'articles/update.html', context)

In the above updateArticle view, we have to check for a PUT request, if we are sending a PUT request, the form instance needs to be loaded from the request object. We use the request.body to access the data sent in the request. The incoming data received from the request.body object is not a valid format to parse it to the form instance, so we will parse it using QueryDict. This will allow us to modify the request.body object into valid python serializable data.

So, we import the QueryDict from django.http module. We parse the data as the parameter to QueryDict and store it in a variable. We then have to get the ArticleForm for fetching the data as per the form details, so we parse the instance and also the data parameter. The instance is the article object and the data is the received form data which we have stored in qd as QueryDict(request.body). This will load the new form data and then we can validate it the form.

After we have verified the form details, we can save the form and this will update the article record. Thereby we can render the updated article in the detail view with the updated article object as the context.

Update View Form Template

So, this will set up the update view as well, we can now create, read, update, and delete an article instance with HTMX in templates and Django function-based views without writing any javascript.

Summary

We were able to create a basic CRUD application in Django with HTMX. We used simple function-based views to demonstrate the inner details of how we can work with HTMX and handle requests from the templates. By creating simple standalone templates, we can connect those together to make a fully functional and responsive webpage. The UI is not great but the purpose of this tutorial was to make a barebone CRUD app to work with the backend using HTMX, so hopefully, you would have got a good overview of how HTMX can be integrated into a Django application.

Overall HTMX is a great library that can be used to enhance or even create a new web application for making the site responsive and without writing any javascript.

Django HTMX CRUD Application Demo GIF

You can check out the source code for this project and blog on the htmx-blog GitHub repository.

Conclusion

From this post, we were able to understand the basics of HTMX and how we can integrate it into a Django application. Hopefully, you enjoyed the post, if you have any queries or feedback, please let me know in the comments or on my social handles. Thank you for reading. Happy Coding :)