# Django + htmx

### Quick intro

[Django](https://docs.djangoproject.com/en/4.2/) is an extremely powerful web framework that comes with nuts and bolts so that you can just focus on what you need to. If you are from a python background, I'm sure you would have used one of [Flask](https://flask.palletsprojects.com/en/3.0.x/) or [Django](https://docs.djangoproject.com/en/4.2/) or [FastAPI](https://fastapi.tiangolo.com/).

I've used it quite a bit to build many solutions, and I was always stunned by the neatness of class-based views, the minimal code that one had to write, the amount of documentation it has, and packages in PyPi that work for any situation one might have.

For the frontend, I use simple HTML and CSS making use of Django template framework, no fancy heavy-duty JS frameworks. Not that they are bad, just that I use a framework depending on the need, and not because the whole world is raving about it.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Using a JS framework or not is purely subjective. Trust me, I love JS frameworks 😁, especially Vue</div>
</div>

Practically speaking, every new framework/technology has a good amount of learning curve associated. If I cannot justify the need 100%, I'd prefer to stay away from it. Instead, I try and find simpler alternatives, weigh the pros and cons and make a final decision.

### A bit of background

One such need for me was to have reactivity on a few screens in a web app developed using `Django`. When I started to dig deeper and find ways to handle it, one option that came up was to shift to JS frameworks for the front end. But it was too overkill in my case. Another option was to use javascript. I skipped that option as it may lead to a messy code over some time.

I then dug further and came across [htmx](https://htmx.org/). Per the htmx website,

> htmx gives you access to [AJAX](https://htmx.org/docs/#ajax), [CSS Transitions](https://htmx.org/docs/#css_transitions), [WebSockets](https://htmx.org/docs/#websockets) and [Server Sent Events](https://htmx.org/docs/#sse) directly in HTML, using [attributes](https://htmx.org/reference/#attributes), so you can build [modern user interfaces](https://htmx.org/examples/) with the [simplicity](https://en.wikipedia.org/wiki/HATEOAS) and [power](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) of hypertext

When I read more about it, I decided to go ahead with it as it is simple, easy to use and does a neat job. In short, it does not load the entire page as in a typical Django way but simply loads the target DOM which is what I wanted.

### Common scenarios to use htmx

There are quite a few scenarios where we do not have to load the entire page, and where we do not have to wait for the response only after we submit the page. Let's look at a few examples:

* Inline validations are quite common in any web app. It makes a lot of sense to know the error message then and there rather than submit it finally
    
* Quick edits are also a very intuitive and easy method to modify information.
    
* Inline deletes and bulk deletes are also becoming common, especially within a table-like structure
    
* Dynamic search is also considered a de facto these days
    
* "Load More" button or "Infinite Scroll" is also more usable than clicking on page numbers using regular pagination
    

These are a few common scenarios that I think will be applicable in most of today's web apps. To look at the complete list of use cases, please take a look at [this link](https://htmx.org/examples/).

Now that we've understood the need to use htmx, let's wet our feet. What's the fun without building something, is it not?

### Time for some action

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Even before you move ahead, I assume you are familiar with Django, which is why I do not elaborate on every single step</div>
</div>

Let us build a super simple version that's intended to be your version of "[Good Reads](https://www.goodreads.com/)", which is why I called it 'My Book Shelf". To start with, let's keep it minimal. Below are the functionalities that we will add:

* Sign up
    
* Sign in/ Sign out
    
* Add a book to your shelf
    
* List books on your shelf
    
* Quick edit using htmx
    
* Delete using htmx
    
* Dynamic search using htmx
    

### Step 1: Bring up a Django project

I recently wrote a post about Django, I'd recommend referring to [my previous post](https://hellosambhavi.com/simple-django-tip-1) to bring up a Django project. Make sure to name the project as `mybookshelf`.

To ensure everything works fine, run the development server to see the default Django page.

### Step 2: Create & Setup a Book app

Let's create a Django app and call it `book`. All logic related to our bookshelf will reside in this app.

```python
python manange.py startapp book
```

Add `urls.py` and `forms.py` under `book` as they do not get added by default.

Create a class `Book` in `models.py`

```python
from django.contrib.auth.models import User
from django.db import models


# Create your models here.
class Book(models.Model):
    options = (
        ("just checking out", "Just Checking Out"),
        ("read", "Read"),
        ("reading", "Reading"),
        ("want to read", "Want to Read"),
    )

    title = models.CharField(max_length=255)
    author = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    description = models.TextField()
    cover = models.ImageField(
        upload_to="covers/", blank=True, verbose_name="Cover Image"
    )
    status = models.CharField(
        max_length=30, choices=options, default="want to read"
    )
    created_by = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="books_createdby"
    )
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    updated_by = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="books_updatedby"
    )
    updated_at = models.DateTimeField(auto_now=True, editable=False)

    class Meta:
        ordering = ("-created_at",)

    def __str__(self):
        return self.title
```

Fields in the `Book` class are self-explanatory.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Do not run <code>makemigrations</code> at this stage as we will add a <code>CustomUser</code> class. We may get compilation errors for referencing User models. Just ignore them for the moment as we shall fix them in a minute.</div>
</div>

Let's now add the methods required in `views.py` and the corresponding `path` in `urls.py`

```python
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.views.decorators.http import require_http_methods
from django.views.generic import CreateView, ListView

from .forms import CustomUserCreationForm
from .models import Book


# Create your views here.
""" Landing page for MyBookShelf """
class HomeView(ListView):
    model = Book
    template_name = "book/index.html"

""" Method invoked for Sign up """
def register(request):
    if request.method == "POST":
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("book:login")
        else:
            form = CustomUserCreationForm()
    return render(
        request, "registration/register.html", {"form": CustomUserCreationForm}
    )

""" Method invoked to list books """
class BookListView(LoginRequiredMixin, ListView):
    model = Book
    template_name = "book/book-list.html"
    context_object_name = "books"
    paginate_by = 10

    def get_queryset(self):
        return Book.objects.filter(created_by=self.request.user)

""" Method invoked to add a book """
class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    template_name = "book/book_create.html"
    fields = ["title", "author", "description", "cover", "status"]

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        form.instance.updated_by = self.request.user
        return super().form_valid(form)

    def get_success_url(self) -> str:
        return reverse_lazy("book:book-list")
```

```python
from django.urls import path

from . import views

app_name = "book"

urlpatterns = [
    path("", views.HomeView.as_view(), name="home"),
    path("login/", auth_views.LoginView.as_view(), name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
    path("register/", views.register, name="register"),
    path("book-list", views.BookListView.as_view(), name="book-list"),
    path("book-create", views.BookCreateView.as_view(), name="book-create"),
]
```

Below is the code for `forms.py`. At the moment I use Django's default username for authentication. Adding CustomUser at this stage so that in the future it can be easily modified to email, or more columns.

```python
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User


class CustomUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = (
            "username",
            "password1",
            "password2",
        )
```

Don't forget to register the model in `admin.py` else it won't be displayed on your Django admin site. Keeping it simple at the moment 😎

```python
from django.contrib import admin

from .models import Book

# Register your models here.
admin.site.register(Book)
```

### Step 3a: Add required template files

I generally maintain templates under the main project. I then create folders, one to hold the common htmls, and one for each app. This way, all templates reside in one place and it's easier this way.

Let's create 3 folders, base, book and registration under the folder templates.

Usually I keep the `base.html` and `navbar.html` under the base folder. `base.html` is more like a template which will be extended by all other `htmls`. This is done more as a DRY (Do not repeat yourself) principle. `navbar.html` holds the html code required for the navigation bar. Here's the code that is present in `base.html`

```xml
{% load static %}

<!doctype html>
<html lang="en">
 
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %} {% endblock %}</title>
    <link href="{% static 'css/main.css' %}" rel="stylesheet">
  </head>

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

</html>
```

And here's the code for `navbar.html`

```xml
<nav class="navbar navbar-expand-lg bg-body-tertiary mx-auto">
    <div class="container-fluid">
      <a class="navbar-brand logo fs-3" href="#">MyBookShelf</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarNavDropdown">
        <ul class="navbar-nav">
        {% if request.user.is_authenticated %}
            <li class="nav-item">
                <a class="nav-link active" aria-current="page" href="{% url 'book:book-list' %}">Home</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'book:book-create' %}">Add a Book</a>
            </li>
            <li class="nav-item">
                <a class="btn button-primary" href="{% url 'book:logout' %}">Sign Out</a>
            </li>
            <li class="nav-item ml-auto">
                <a class="nav-link">Welcome {{ request.user }} !</a>
            </li>
        {% else %}
            <li class="nav-item">
                <a class="nav-link active" aria-current="page" href="#">Home</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'book:login' %}">Sign In</a>
            </li>
            <li class="nav-item">
                <a class="btn button-primary" href="{% url 'book:register' %}">Sign Up</a>
            </li>
        {% endif %}
        </ul>
      </div>
    </div>
  </nav>
```

Let's now create authentication related html files under `registration` folder. We need a `login.html`, a `logout.html` and a `register.html`. Just to keep things simple, I did not add any content to `logout.html` as on logout I just map it to the homepage (`book\index.html`).

Here's the code for `login.html`

```xml
{% extends "../base/base.html" %}

{% block title %} Sign in {% endblock %}


{% block content %}
    {% include "../base/navbar.html" %}
    
    {% if form.errors %}
        <p>Your username and password does not match 🤨</p>
    {% else %}
        <h3 style="text-align: center;color: #6a00f4">Sign In</h3>
    {% endif %}

    <div class="container text-center">
        <div class="row align-items-start">
            <div class="col"></div>
            <div class="col">
                <div class="login-form">
                    <form action="{% url 'book:login' %}" method="post">
                        {% csrf_token %}
                        {{ form.as_p }}
                        <button type="submit" class="btn button-primary">Sign in</button>
                    </form>
                </div>
            </div>
            <div class="col"></div>
    </div>
    </div>
{% endblock %}
```

Here's the code related to Sign up, it should be added to `registration\register.html`.

```xml
{% extends "../base/base.html" %}

{% block title %} Sign in {% endblock %}


{% block content %}
    {% include "../base/navbar.html" %}
    
    {% if form.errors %}
        <p>There is an error. Please correct and re-submit</p>
        <ul class="errorlist">
            {% for field, errors in form.errors.items %}
                {% for error in errors %}
                    <li>{{ error }}</li>
                {% endfor %}
            {% endfor %}
            </ul>
    {% else %}
        <h3 style="text-align: center;color: #6a00f4">Sign Up</h3>
    {% endif %}

    <div class="container text-center">
        <div class="row align-items-start">
            <div class="col"></div>
            <div class="col">
                <div class="login-form">
                    <form action="{% url 'book:register' %}" method="post">
                        {% csrf_token %}
                        {{ form.as_p }}
                        <button type="submit" class="btn button-primary">Sign up</button>
                    </form>
                </div>
            </div>
            <div class="col"></div>
    </div>
    </div>
{% endblock %}
```

Now for the default home page code, which is under `book\index.html`

```xml
{% extends "../base/base.html" %}
{% load static %}


{% block title %} My Book Shelf {% endblock %}

{% block content %}
    {% include "../base/navbar.html" %}
    <div class="container text-center" >
        <div class="row align-items-start">
            <div class="col col-margin">
                <h3>Want to maintain your own version of Good Reads?</h1>
                <p class="lead">Add books, categorize them as </p>
                <p style="color: #f20089"> Want to Read / Reading / Read /Just checking</p>
                <p class="lead">Sign up and keep track of your reading habits in your own book shelf !</p>
                <a class="btn button-primary" href="{% url 'book:register' %}">Sign Up</a>
            </div>
            <div class="col col-margin">
                <img src="{% static 'images/bookshelf.png' %}" alt="bookshelf" class="img-fluid">
            </div>
        </div>
    </div>
{% endblock %}
```

Once the htmls are developed, we then move on to create static files. I usually create `static` folder in the main project. Create 3 folders, one for `css`, one for `images` and one for `js`.

This is how my folder structure looks like with all the folders and files.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note that the image below has a few additional files too which I did not discuss until now. Not to worry, they will be covered in the following sections, or you can find them in my <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/SambhaviPD/mygoodreads/tree/main/mybookshelf/mybookshelf" style="pointer-events: none">github repo</a>.</div>
</div>

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1701864255456/82731c44-45d4-4562-9fd0-d51baa0b32da.png align="center")

We also need to make sure that Django is able to navigate to the templates folder. In order to do that, add the following against `DIRS` property under `TEMPLATES` in your `base.py.`

It should now look like 👇

```xml
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",
            ],
        },
    },
]
```

### Step 4: Make a few additions to the main project

Now that we are done with our `Book` app, let's make sure that our main project knows about this app. We also need to make sure to add properties related to `static` files and `media` files.

* Under `INSTALLED_APPS` in `base.py`, add `book`
    
* ```python
          STATIC_URL = "static/"
          
          STATICFILES_DIRS = [BASE_DIR / "static"]
          
          MEDIA_URL = "/media/"
          
          MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
    ```
    
    ```python
    LOGIN_REDIRECT_URL = reverse_lazy("book:book-list")
    LOGOUT_REDIRECT_URL = "/"
    LOGIN_URL = "/login"
    LOGOUT_URL = "/logout"
    ```
    
    Make sure to update the `urls.py` like so to reference the newly created app as well as the path to `static` and `media` files 👇
    
* ```python
          from django.conf import settings as base
          from django.conf.urls.static import static
          from django.contrib import admin
          from django.urls import include, path
          
          urlpatterns = [
              path("admin/", admin.site.urls),
              path("", include("book.urls", namespace="book")),
          ]
          
          if base.DEBUG:
              urlpatterns += static(base.MEDIA_URL, document_root=base.MEDIA_ROOT)
    ```
    

We've now created the base project along with an app with Add and List functionality. Here is how the landing page looks like:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697453061609/ecf7f2d6-2fef-4a09-9397-7779dd779c7d.png align="center")

I signed up, and added a couple of books, and it looks like 👇

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697453164344/7453b018-8a8e-4a08-8947-e9965675d40e.png align="center")

Let's now get to the most important part of this post, the `htmx` part. To get a good grasp of `htmx` I highly recommend going through their [documentation](https://htmx.org/docs/).

### Step 5: Let's set up htmx in our bookshelf 🚀

Install `django-htmx` in your virtual environment

```python
pip install django-htmx
```

Add it to your installed apps

```python
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django_htmx",
    "book",
]
```

Add `HTMXMiddleware` to the `MIDDLEWARE`section

```python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "django_htmx.middleware.HtmxMiddleware",
]
```

Make sure to add the following to your `base.html.`

The first line is to include the actual js file. The second line is to handle `csrf_token` as Django requests will not function without it.

```python
    <script src="{% static 'js/htmx.min.js' %}" defer></script>
    <script>
      document.body.addEventListener('htmx:configRequest', (event) => {
        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
      })
    </script>
```

### Step 6: Adding htmx code to our book app

A few important properties of `htmx` that I used are:

* hx-post - Invokes a post request
    
* hx-get - Invokes a get request
    
* hx-delete - Invokes a delete request
    
* hx-target - Which part of the DOM should be replaced with the response?
    
* hx-swap - HTML to be replaced in the DOM
    
* hx-trigger - What triggers the event?
    

To get a mental mapping to use htmx, I follow the steps below:

* Identify the element in a page that needs reactivity
    
* Separate the output section corresponding to that element
    
* Move that to a separate `html` file. Usually, the norm is to create a folder called `partials` under the usual `templates` folder.
    
* Include the newly created `html` file in the parent file so that the contents will be replaced
    
* In the parent `html` file, add `htmx-`related properties including the type of method to be invoked, the target element, what to swap, when to trigger and so on.
    

In our bookshelf, let's add the functionality of a quick edit of a few details of the book and deletion of the book without reloading the entire page.

Make the following changes to the existing `book_list.html`

Notice how we invoke the code to populate the table in separate `book.html`

```xml
{% extends "../base/base.html" %}

{% block content %}
    {% include "../base/navbar.html" %}
    <h3 style="text-align: center;">A peek into your book shelf!</h3>
    <section id="book-list">
        <div class="container">
            <div class="row border-bottom pb-2 mb-3 custom-header-bg text-white">
                <div class="col-2">Title</div>
                <div class="col-2">Author</div>
                <div class="col-2">Status</div>
                <div class="col-3">Modify?</div>
                <div class="col-3">Remove?</div>
            </div>
        </div>  
        {% for book in books %}
            {% include "../book/partials/book.html" %}
        {% empty %}
            <p style="text-align: center;">No books in your shelf 🙈</p>
        {% endfor %}
        </div>
    </section>
{% endblock %}
```

* Let's now create a file `book.html` under `the templates\partials` folder. Add the following content
    
* ```xml
          <div class="container">
              <article>
                  <div class="row mb-3">
                      <div class="col-2">{{ book.title }}</div>
                      <div class="col-2">{{ book.author }}</div>
                      <div class="col-2">{{ book.status }}</div>
                      <div class="col-3">
                          <form 
                              hx-get="{% url 'book:quick-edit' pk=book.pk %}"
                              hx-target="closest article"
                              hx-swap="outerHTML">
                                  <button class="btn button-secondary">Quick edit</button>
                          </form>
                      </div>
                      <div class="col-3">
                          <button class="btn button-secondary" 
                          hx-confirm="Are you sure you want to delete this book?"
                          hx-target="closest article"
                          hx-swap="outerHTML swap:1s"
                          hx-delete="books/{{ book.id }}/delete">
                          Delete
                          </button>
                      </div>
                  </div>
              </article>
          </div>
    ```
    
    In the case of the Quick edit button, we invoke a `htmx-get` to load the details of the book. The method also takes in a post method (in `views.py`) when the form is submitted. The `hx-target` maps to `closest article` in this case which refers to the `article` tag. It need not always be a tag. It can be an identifier too.
    
* In the case of the Delete button, it is quite similar to the above except that we invoke `hx-delete` method. We also open up a small modal window that asks for a confirmation.
    
* <div data-node-type="callout">
    <div data-node-type="callout-emoji">💡</div>
    <div data-node-type="callout-text">Make sure to invoke the correct method from <code>views.py</code></div>
    </div>
    

That is all, quite simple! Initially, it takes a bit to wrap your head around htmx 😅. But that's the same with anything new, is it not? Once we understand the knack of things, it's just a matter of splitting, writing a few properties and invoking the child `html`.

Since the post is quite long already, I'm not adding the code snippet here related to Dynamic search. It's quite similar to the above. I will add a reference to my `Github` repo in the end, just in case you are interested to see the entire code 😀

### Closing thoughts

`htmx` is a powerful library that can be used with many backend frameworks. I use it with `Django`, I'm sure it can be used with `Flask` too. I highly recommend using it when you have a case of loading a part of the page, firing validation as and when a user types something, and other reactive behavior.

As a matter of habit, I choose a framework/library only when the need is justified 200%, otherwise to me it's a better option to stick to a lightweight alternative as long as it satisfies my requirement.

Here is a link to [my Github repo](https://github.com/SambhaviPD/mygoodreads).

### References

[Simple working example of Django + htmx : Part 1](https://www.youtube.com/watch?v=Pr8z9XxyrJc&list=PLpyspNLjzwBlqsqXhAZ7zr3zJ8vLqQFWP)

[Simple working example of Django + htmx : Part 2](https://www.youtube.com/watch?v=PeEwO56gNy8&list=PLpyspNLjzwBlqsqXhAZ7zr3zJ8vLqQFWP&index=4)

[Article on dynamic search using htmx](https://fly.io/django-beats/a-no-js-solution-for-dynamic-search-in-django/)
