Infinite Scroll Pagination in Django with HTMX
Tags: django, htmx, webdev, python
Published: 2023-10-14 07:20:45

A few years ago I made a webapp in Django.
I also added the pagination in it. But something was missing.

Yes, it was **Infinite Scroll Pagiantion**

So in this todays blogpost, we are going to be making an Infinite Scroll Pagination in Django using HTMX

Also, the paginator will still work even if the users Javascript is disabled

----

Firstly let's create a django project and a django app

 1. Install django

```ps
python -m pip install django
```
2. Creating a django app and project

```ps
django-admin startproject InfiniteScroll

cd ./InfiniteScroll

python manage.py startapp core
```

3. Adding our django app in the INSTALLED_APP in `InfiniteScroll/setting.py`

```py
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Our Apps
    'core.apps.CoreConfig' #<-- ADD THIS
]

```

4. To help with some things let's create a utility file named `utils.py` in our core app.

```py
from django.core.paginator import Paginator


def is_htmx(request, boost_check=True):
    hx_boost = request.headers.get("Hx-Boosted")
    hx_request = request.headers.get("Hx-Request")
    if boost_check and hx_boost:
        return False

    elif boost_check and not hx_boost and hx_request:
        return True


def paginate(request, qs, limit=2):
    paginated_qs = Paginator(qs, limit)
    page_no = request.GET.get("page")
    return paginated_qs.get_page(page_no)
```

The `is_htmx` will check if the request is originating from htmx or not. It will also check if the request is [boosted](https://htmx.org/docs/#boosting)

The paginate function will help us to paginated the queryset. You can adjust the limit parameter if you want. For testing purposes I set the limit to `2`

5. In the `views.py` add this

```py
from django.shortcuts import render

from django.contrib.auth.models import User

from .utils import is_htmx, paginate

# Create your views here.


def index(request):
    users = paginate(request, User.objects.all())
    if is_htmx(request):
        return render(request, "islands/user_list.html", {"users": users})
    return render(request, "index.html", {"users": users})
```

Here we are returning the user list and if the request is a htmx request then we are returning the partials

6. Let's Create the templates

`templates/islands/pagination.html`

```html
<div {% if page.has_next %} hx-get="?page={{ page.next_page_number }}" hx-trigger="revealed"
hx-swap="outerHTML" {% endif %}>
    {% if page.has_next or page.has_previous %}
    <nav class="pagination is-centered is-small mt-2" role="navigation" aria-label="pagination">
        <ul class="pagination-list">
            {% if page.has_previous %}
            <a class="pagination-link" aria-label="Goto page 1" href="?page=1">First Page</a>
            <a class="pagination-link" href="?page={{ page.previous_page_number }}">«
                Previous</a>
            {% endif %}
            <a class="pagination-link is-current" aria-label="Goto page {{ page.number }}">{{ page.number }}</a>
            {% if page.has_next %}
            <a class="pagination-link" href="?page={{ page.next_page_number }}">Next page
                »</a>
            <a class="pagination-link" aria-label="Goto page 4&amp;query="
                href="?page={{ page.paginator.num_pages }}">Last
                Page</a>
            {% endif %}
        </ul>
    </nav>
    {% endif %}
<script>
    htmx.on("htmx:load", function (evt) {
        document.querySelector(".pagination").style.display = "none";
    });
</script>
</div>
```

The `.pagination` element is have the following attributes
 - `hx-get`: The link to the next page
 - `hx-trigger`: The trigger event that will call the endpoint
 - `hx-swap`: The type of swap to occur. Here it's outerHTML

This code is pretty simple.
Firstly we are checking if there is a next page or not using django, then if there is a page then we are calling the `?page={{ 
page.next_page_number }}`, for example, if the current page is `1` then the `{{ page.next_page_number }}` will return `2` and so on

Next we have the type of trigger event.
It means which events should occur to fire the request from the htmx. In this case its `revealed` means when the pagination element is revealed then htmx will fire the request. [More Info](https://htmx.org/attributes/hx-trigger/)

And finally we have our `hx-swap`, means when the htmx will get the response we want to replace all the html in `.pagination` with the response html
[More Info](https://htmx.org/attributes/hx-swap/)

Here we are hiding the `.pagination` on every htmx request. If the js is disabled then the user will get the pagination buttons.

This is called [Progressive Enhancement](https://htmx.org/docs/#progressive_enhancement)


`templates/islands/user_list.html`

```html

{% for user in users %}
<div class="box">
    <p class="subtitle is-1">{{ user }}</p>
    
    {% if user.is_staff %}
        <span class="tag is-success">Staff</span>
    {% endif %}
        
</div>
    <br>
{% endfor %}
{% include 'islands/pagination.html' with page=users %}
```

This is just a simple django template file which will take the list of users and show them using a for-loop

We also have our pagination template which will be renderd after the user list.


`templates/index.html`

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Infinite Scroll Pagination</title>
    <script src="https://unpkg.com/htmx.org@1.9.6"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
    <link rel="stylesheet" type="text/css" href="https://unpkg.com/bulma-prefers-dark" />

</head>
<body>
    <div class="container">
        <section class="section">

            {% include 'islands/user_list.html' %}
        </section>
    </div>
        
</body>
</html>
```

Here we are using the [bulma](https://bulma.io) and [bulma-prefers-dark](https://github.com/jloh/bulma-prefers-dark) to style our page

As you can see we can use our partial templates anywhere in our code so we can just include the `islands/user_list.html` in the `index.html`

7. Now finally hook it up in the `urls.py`

`InfiniteScroll/urls.py`

```py
from django.contrib import admin
from django.urls import path

from core.views import index  #<-- ADD THIS

urlpatterns = [
    path('admin/', admin.site.urls),

    path('', index), #<-- ADD THIS
]

```

Import the `index` view from `core.views` and map it in the `urlpatterns`

8. Now lets create a superuser to login in the admin panel

```ps
python manage.py createsuperuser
```

9. Run the server and got to [http://localhost:8000/admin/auth/user/](http://localhost:8000/admin/auth/user/) to add some more users
and then go to the [index page](http://localhost:8000/) to check it

```ps
python manage.py runserver
```


![User Add in admin panel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4xht3nzpsqo40x600nur.jpeg) *User Add in admin panel*



![Index page, with javascript](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fnoovo1k1u6ziab89nz0.jpeg) *Index page, with javascript*



![Index page, without javascript](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mmv88m04l4vnlh3498yt.jpeg) *Index page, without javascript*


#### DEMO
{% embed https://youtu.be/JBse978l0gk %}

---
Phew!

So thats how you can do it.

Bye.

Peace ✌
@foxy4096 out.