Responsive table with Django and htmx
Tags: django, htmx
Published: 2022-03-17 06:41:25

---
title: Responsive table with Django and htmx
published: true
date: 2022-03-17 04:04:47 UTC
tags: #django #htmx
canonical_url: https://enzircle.com/responsive-table-with-django-and-htmx
---

As a developer, you will need to create tables in your web applications. To create a table in Django, you typically implement a set of APIs on the server-side that transfer data to the client and use a Javascript table library on the client-side. But you can also implement an HTML table. The drawback of this approach is that every action like sorting or searching will refresh the whole page. And that can be a jarring experience for the user. But did you know that there's a better way to create tables? This article will show you how to use Django and `htmx` to develop functional and responsive tables.

This article will show you how to build a table using `django-tables2` and `htmx`. The table I am going to implement looks like this:

![Table sketch](https://cdn.hashnode.com/res/hashnode/image/upload/v1647498612286/BFDKpWPUh.png)

I assume you are already familiar with Django and can set up a Django project on your own. So I will not go through setting up a Django project from scratch. Let's get started.

## The packages used for this project

1. [htmx](https://htmx.org/). `htmx` is a small Javascript that helps you avoid Javascript. Javascript fatigue is real. There are too many Javascript front-end frameworks, tools, and options. Integrating some of this framework into a Django project brings a lot of complexity. You will need to context switch between Python and Javascript. You also need to understand a broad set of tools like `node`, `npm`, `webpack`, etc. `htmx` lets Django developer sticks to what Django is good at: the server-side stuff.
2. [django-tables2](https://github.com/jieter/django-tables2). You can implement an HTML table using just Django, but there is a way to create and manage tables easily: `django-tables2`. This Django app lets you define tables like you define Django models. It can automatically generate a table based on a Django model. It supports pagination, column-based table sorting, custom column functionality via subclassing, and many other features.
3. [django-filter](https://github.com/carltongibson/django-filter). I use this package for the search functionality. It has API that is similar to Django's `ModelForm` and works well with `django-tables2`.
4. [django-htmx](https://github.com/adamchainz/django-htmx). For `htmx` to work, Django `view` needs to be able to tell which request is made using `htmx` and which is not. You can make your middleware or a class that your view can inherit to handle this. But for this project, I use `django-htmx` because why reinvent the wheel. It has a middleware that adds `htmx` attribute to a `request` object.

## How htmx works

Before we move on, let's talk about how `htmx` works. The `htmx` library gives you access to AJAX. In vanilla HTML, only `<a>` and `<form>` can make HTTP requests, and only click and submit events can trigger them. Furthermore, an HTTP requests will always replace the entire screen. But the `htmx` library has overcome all these limitations. You can initiate HTTP requests on any element you want. And it is straightforward to do, example (taken straight from the [htmx page](htmx.org)):

```html
  <!-- Load from unpkg -->
  <script src="https://unpkg.com/htmx.org@1.7.0"></script>
  <!-- have a button POST a click via AJAX -->
  <button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  </button>

```

When you click on the button, `htmx` will issue an AJAX request to `/clicked`, and replace the entire button with the HTML response.

I can use the same mechanics for a table. All I need to do:

1. Render the whole page with the first page of the table
2. When you trigger any action (sort, search, go to a specific page), it will issue an AJAX request to the server
3. Server returns HTML response (partial HTML, only the table data)
4. Designated section content gets replaced with the new HTML response

For this project, I use several `htmx` attributes, they are:

1. `hx-get`. To issue a GET request to a given URL.
2. `hx-trigger`. The way to trigger the request.
3. `hx-target`. To load the result into a target element.
4. `hx-swap`. To swap the HTML returned into the DOM method.
5. `hx-indicator`. Let the user know that something is happening since the browser will not give them any feedback.

## The model

First, let's create a simple product table. I will make a table out of the following model.

```python
# products/models.py
from django.db import models

class Product(models.Model):
    class Status(models.IntegerChoices):
        ACTIVE = 1, "Active"
        INACTIVE = 2, "Inactive"
        ARCHIVED = 3, "Archived"

    name = models.CharField(max_length=255)
    category = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.PositiveSmallIntegerField(choices=Status.choices)

    def __str__ (self):
        return self.name

```

## The table

I will now define a table and specify the template. This template override is necessary because I need to add some `htmx` attributes to some elements. My table definition looks like the following.

```python
# products/tables.py
import django_tables2 as tables
from products.models import Product

class ProductHTMxTable(tables.Table):
    class Meta:
        model = Product
        template_name = "tables/bootstrap_htmx.html"

```

Inside the `bootstrap_htmx.html` template, I extend the original `bootstrap4.html` template. The parts that I extend are the table headers (used for sorting) and the pagination part. Notice that the `hx-target` points to `div.table-container`. This is because the `bootstrap4.html` template has a `div` container that wraps the table and the class for the container is `table-container`. The `htmx` will swap the data of `div.table-container` with the response from the server.

```html
{# templates/tables/bootstrap_htmx.html #}
{% extends "django_tables2/bootstrap4.html" %}

{% load django_tables2 %}
{% load i18n %}

{% block table.thead %}
  {% if table.show_header %}
      <thead {{ table.attrs.thead.as_html }}>
      <tr>
          {% for column in table.columns %}
              <th {{ column.attrs.th.as_html }}
                  hx-get="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
                  hx-trigger="click"
                  hx-target="div.table-container"
                  hx-swap="outerHTML"
                  hx-indicator=".progress"
                  style="cursor: pointer;">
                  {{ column.header }}
              </th>
          {% endfor %}
      </tr>
      </thead>
  {% endif %}
{% endblock table.thead %}

{# Pagination block overrides #}
{% block pagination.previous %}
    <li class="previous page-item">
        <div hx-get="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"
             hx-trigger="click"
             hx-target="div.table-container"
             hx-swap="outerHTML"
             hx-indicator=".progress"
             class="page-link">
            <span aria-hidden="true">&laquo;</span>
            {% trans 'previous' %}
        </div>
    </li>
{% endblock pagination.previous %}
{% block pagination.range %}
    {% for p in table.page|table_page_range:table.paginator %}
        <li class="page-item{% if table.page.number == p %} active{% endif %}">
            <div class="page-link" 
                 {% if p != '...' %}hx-get="{% querystring table.prefixed_page_field=p %}"{% endif %}
                 hx-trigger="click"
                 hx-target="div.table-container"
                 hx-swap="outerHTML"
                 hx-indicator=".progress">
                {{ p }}
            </div>
        </li>
    {% endfor %}
{% endblock pagination.range %}
{% block pagination.next %}
    <li class="next page-item">
        <div hx-get="{% querystring table.prefixed_page_field=table.page.next_page_number %}"
             hx-trigger="click"
             hx-target="div.table-container"
             hx-swap="outerHTML"
             hx-indicator=".progress"
             class="page-link">
            {% trans 'next' %}
            <span aria-hidden="true">&raquo;</span>
        </div>
    </li>
{% endblock pagination.next %}

```

## The filter

For adding filtering and searching on a `django-tables2` table, I will use `django-filter`. The following is the filter definition.

```python
# products/filters.py
from decimal import Decimal
from django.db.models import Q
import django_filters
from products.models import Product

class ProductFilter(django_filters.FilterSet):
    query = django_filters.CharFilter(method='universal_search',
                                      label="")

    class Meta:
        model = Product
        fields = ['query']

    def universal_search(self, queryset, name, value):
        if value.replace(".", "", 1).isdigit():
            value = Decimal(value)
            return Product.objects.filter(
                Q(price=value) | Q(cost=value)
            )

        return Product.objects.filter(
            Q(name__icontains=value) | Q(category__icontains=value)
        )

```

Since there is only a single search form for the entire table, I first check if the input data is a digit. If it is, only search for columns `price` and `cost`. Otherwise, search in columns `name` and `category`.

## The view

The view will send out a full page when the request is not made by `htmx` and send partial results when the request is made by `htmx`.

```python
# products/views.py
from django_tables2 import SingleTableMixin
from django_filters.views import FilterView

from products.models import Product
from products.tables import ProductHTMxTable
from products.filters import ProductFilter

class ProductHTMxTableView(SingleTableMixin, FilterView):
    table_class = ProductHTMxTable
    queryset = Product.objects.all()
    filterset_class = ProductFilter
    paginate_by = 15

    def get_template_names(self):
        if self.request.htmx:
            template_name = "product_table_partial.html"
        else:
            template_name = "product_table_htmx.html"

        return template_name

```

The template for `product_table_htmx.html` will render the entire page complete with the HTML header. The `render_table` template tag will generate the table according to our `tables/bootstrap_htmx.html` template.

```python
{# product/templates/product_table_htmx.html #}
{% extends "base.html" %}

{% load render_table from django_tables2 %}
{% load i18n %}
{% load crispy_forms_tags %}

{% block main %}
    <h1>Product table</h1>

    {# Search form #}
    <form hx-get="{% url 'product_htmx' %}" 
          hx-target="div.table-container" 
          hx-swap="outerHTML" 
          hx-indicator=".progress" 
          class="form-inline">
        {% crispy filter.form %}
    </form>

    {# Progress indicator #}
    <div class="progress">
        <div class="indeterminate"></div>
    </div>

    {# The actual table #}
    {% render_table table %}
{% endblock %}

```

The template for `product_table_partial.html` will render only the table part. It will return only the table container part and the table itself. Notice we do not extend from the `base.html`.

```html
{# product/templates/product_table_partial.html #}
{% load render_table from django_tables2 %}

{% render_table table %}

```

## Style

The table we define already uses Bootstrap 4 table styling. But I need to add some style so I can see an arrow on the right of the column when I perform sorting on a column. The arrow will point up or down depending on whether the sort is ascending or descending. Also I need to specify the width of the column, otherwise the column will change size during sorting or searching. After that I only need to add styling for the progress indicator.

```css
/* Table style */
.table-container th.asc:after {
    content: '\0000a0\0025b2';
    float: right;
}

.table-container th.desc:after {
    content: '\0000a0\0025bc';
    float: right;
}

.table-container table td:nth-child(1) {
    width: 5%;
}

.table-container table td:nth-child(2) {
    width: 20%;
}

.table-container table td:nth-child(3) {
    width: 50%;
}

/* Progress bar */
.progress {
    height: 4px;
    width: 100%;
    border-radius: 2px;
    background-clip: padding-box;
    overflow: hidden;
    position: relative;
}

.progress {
    opacity: 0;
}

.htmx-request .progress {
    opacity: 1;
}
.htmx-request.progress {
    opacity: 1;
}

.progress .indeterminate {
    background-color: blue;
}

.progress .indeterminate:before {
    content: '';
    position: absolute;
    background-color: inherit;
    top: 0;
    left: 0;
    bottom: 0;
    will-change: left, right;
    -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
    animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
}
.progress .indeterminate:after {
    content: '';
    position: absolute;
    background-color: inherit;
    top: 0;
    left: 0;
    bottom: 0;
    will-change: left, right;
    -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
    animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
    -webkit-animation-delay: 1.15s;
    animation-delay: 1.15s;
}

@keyframes indeterminate {
    0% {
        left: -35%;
        right: 100%;
    }
    60% {
        left: 100%;
        right: -90%;
    }
    100% {
        left: 100%;
        right: -90%;
    }
}

@keyframes indeterminate-short {
    0% {
        left: -200%;
        right: 100%;
    }
    60% {
        left: 107%;
        right: -8%;
    }
    100% {
        left: 107%;
        right: -8%;
    }
}

```

## The result

So now I have a working table that looks like this.

### Sorting

![Sorting](https://cdn.hashnode.com/res/hashnode/image/upload/v1647498656802/bInzuN9SO.gif)

### Pagination

![Pagination](https://cdn.hashnode.com/res/hashnode/image/upload/v1647498665540/4PrjH6BDp.gif)

### Searching

![Search](https://cdn.hashnode.com/res/hashnode/image/upload/v1647498680448/RlhTRftmp.gif)

## Summary

In this article, I have shown why you should take a step back in frontend development and carefully weigh the pros and cons of introducing a Javascript framework into a Django project. I have shown you how to use `htmx` with `django-tables` to create a responsive and functional table, so you can keep the frontend light and lean.