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
- Install django
python -m pip install django
- Creating a django app and project
django-admin startproject InfiniteScroll
cd ./InfiniteScroll
python manage.py startapp core
- 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
]
- 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)
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
- 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})
Here we are returning the user list and if the request is a htmx request then we are returning the partials
- 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&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={{
, for example, if the current page is
page.next_page_number }}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 %}
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>
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
- 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
]
Import the index
view from core.views
and map it in the urlpatterns
- Now lets create a superuser to login in the admin panel
python manage.py createsuperuser
- 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
Index page, without javascript
DEMO
Phew!
So thats how you can do it.
Bye.
Peace ✌
@foxy4096 out.
Top comments (3)
In the next blogpost, I will be showing how to write a markdown input widget with live preview using HTMX
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.Wow I really couldn't think of that.
Thanks for the tip.