DEV Community

Cover image for Infinite Scroll Pagination in Django with HTMX
Foxy4096
Foxy4096

Posted on

Infinite Scroll Pagination in Django with HTMX

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
python -m pip install django
Enter fullscreen mode Exit fullscreen mode
  1. Creating a django app and project
django-admin startproject InfiniteScroll

cd ./InfiniteScroll

python manage.py startapp core
Enter fullscreen mode Exit fullscreen mode
  1. Adding our django app in the INSTALLED_APP in InfiniteScroll/setting.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
]

Enter fullscreen mode Exit fullscreen mode
  1. To help with some things let's create a utility file named utils.py in our core app.
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)
Enter fullscreen mode Exit fullscreen mode

The is_htmx will check if the request is originating from htmx or not. It will also check if the request is boosted

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

  1. In the views.py add this
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})
Enter fullscreen mode Exit fullscreen mode

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

  1. Let's Create the templates

templates/islands/pagination.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>
Enter fullscreen mode Exit fullscreen mode

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

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

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

templates/islands/user_list.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 %}
Enter fullscreen mode Exit fullscreen mode

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

<!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>
Enter fullscreen mode Exit fullscreen mode

Here we are using the bulma and 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

  1. Now finally hook it up in the urls.py

InfiniteScroll/urls.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
]

Enter fullscreen mode Exit fullscreen mode

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

  1. Now lets create a superuser to login in the admin panel
python manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode
  1. Run the server and got to http://localhost:8000/admin/auth/user/ to add some more users and then go to the index page to check it
python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

User Add in admin panel User Add in admin panel

Index page, with javascript Index page, with javascript

Index page, without javascript Index page, without javascript

DEMO


Phew!

So thats how you can do it.

Bye.

Peace ✌
@foxy4096 out.

Top comments (3)

Collapse
 
foxy4096 profile image
Foxy4096

In the next blogpost, I will be showing how to write a markdown input widget with live preview using HTMX

Collapse
 
turculaurentiu91 profile image
Turcu Laurentiu

Nice post! You can wrap the normal pagination with a <noscript> to preserve the normal functionality when JS is disabled instead of that inline script.

Collapse
 
foxy4096 profile image
Foxy4096

Wow I really couldn't think of that.
Thanks for the tip.