Pagination HTMX partial

Django 5.1.4
Python 3.12.4

Hi Guys, Once again I seek advice from you. Basically doing a partial page load (table) using HTMX and have no issues doing that. It’s when I try to do pagination. Either the paging won’t page forward but does show there are 2 pages (page_obj : <Page 1 of 2>), or streams rows off the page indefinitely or the page control UI itself does not show at all. I have tried all I can come up with to get this to work.

Outline:
I’m loading / setting up an initial page (base_console_ap.html) then have an hx-get to a view(console_view - no data returned) to ‘load’ a partial page (task_index.html) this also includes a partial page (task_list.html) some table elements <thead> <tbody> with hx-get to task_index view that returns data and loads task_row.html etc…
The code below shows the first page of data and the paging UI with a page count of 2 but will not page forward.

base_console_ap.html

<div class="container-fluid rounded border" style="max-width:1100px; height:510px">
            <div class="row">
                <div class="col">
                  <div id="task_indx"  
                  hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
                  hx-trigger="load" 
                  hx-get="{% url 'console_app:console_view' %}" 
                  hx-target="this"> 
                </div>
            </div>
</div>

views.py(console_view)

def console_view(request):
    
    if request.htmx:
        template = 'console_app/task_index.html'
        return render(request, template, {})
    else:
        data = []
        fls_msg_cnt = ManagerMessages.objects.filter().count()
        nodes_cnt = Nodes.objects.filter().count()
        ToLog_cnt = ToLog.objects.filter().count()
        data = {
            'labels': ['Messages', 'Ivanti', 'SolarWinds', 'Control-M', 'TurnOver Log'],
            'values': [fls_msg_cnt, nodes_cnt, 28, 21, ToLog_cnt] 
        }
        template = 'console_app/base_console_ap.html'
        return render(request, template, {'chart_data': data})

views.py (task_view)

def task_index(request):
    if request.htmx:
        print('htmx - task_index')
        tasks = Task.objects.all().order_by(_TASK_ORDBY)   
        paginator = Paginator(tasks, 4)  
        page_number = request.GET.get('page')  
        try:
            page_obj = paginator.page(page_number) 
        except PageNotAnInteger:
            page_obj = paginator.page(1) 
        except EmptyPage:
            page_obj = paginator.page(paginator.num_pages)
        template = 'console_app/task_row.html'
        return render(request, template, {'page_obj': page_obj, 'count': tasks.count()})  

task_index.html

<div id="taskList">
    {% include 'console_app/task_list.html' %}
</div>

task_list.html

<tbody id="taskrows" hx-trigger="load" hx-get="{% url 'console_app:task_index' %}" hx-target="this">
                <tr>
                    <td class="spinner-border" role="status">
                    <span class="visually-hidden">Loading please wait...</span>
                    </td>
                </tr>
</tbody>

task_row.html

{% for task in page_obj %}

<tr>
  {% if task.completed %}
    <td style="word-wrap: break-word;min-width: 160px;max-width: 260px;"><s>{{ task.task }}</s></td>
  {% else %}
    <td style="word-wrap: break-word;min-width: 160px;max-width: 260px;">{{ task.task }}</td>
  {% endif %}
  
  <!-- <td>{{ task.completed }}</td> -->
  <td>{{ task.created_on }}</td> 
  <td>{{ task.notes }}</td> 
  <td>
  {% if task.completed %} 
    <button type="button" class="btn-sm btn btn-outline-secondary" disabled>&checkmark;</button>
  {% else %}
    <button type="button" class="btn-sm btn btn-outline-success" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-get="{% url 'console_app:mark_task' task.pk %}" hx-target="#taskList" style="cursor: pointer;">✔</button>
  {% endif %}

    <button type="button" 
     class="btn-sm btn btn-outline-danger" 
     hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
     hx-get="{% url 'console_app:delete_task' task.pk %}" 
     hx-target="#taskList" 
     hx-confirm="Are you sure you wish to delete?" 
     style="cursor: pointer;">
     DEL
    </button>
  
    <button type="button"
     class="btn-sm btn btn-outline-warning" 
     hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
     hx-get="{% url 'console_app:edit_task' task.pk %}" 
     hx-target="closest tr" 
     hx-swap="outerHTML" 
     style="cursor: pointer;">
     Edit
    </button>
  </td>
</tr>
{% endfor %}

<div class="btn-group" role="group" aria-label="Item pagination">
  {% if page_obj.has_previous %}
      <a href="?page={{ page_obj.previous_page_number }}" class="btn btn-outline-primary">&laquo;</a>
  {% endif %}

  {% for page_number in page_obj.paginator.page_range %}
      {% if page_obj.number == page_number %}
          <button class="btn-sm btn btn btn-primary active">
              
              <span>{{ page_number }} <span class="sr-only">(current)</span></span>
          </button>
      {% else %}
          <a href="?page={{ page_number }}" class="btn btn-outline-primary">
              {{ page_number }}
          </a>
      {% endif %}
  {% endfor %}

  {% if page_obj.has_next %}
      <a href="?page={{ page_obj.next_page_number }}" class="btn btn-outline-primary">&raquo;</a>
  {% endif %}
</div>
<div>Total record count: {{ count }}</div>

The pagination links need to trigger HTMX requests and update the correct target.

First, modify your task_index.html to include a container for the table and pagination:

<div id="task-container">
    <table class="table">
        <thead>
            <tr>
                <th>Task</th>
                <th>Created On</th>
                <th>Notes</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody id="taskList"
               hx-trigger="load"
               hx-get="{% url 'console_app:task_index' %}"
               hx-target="this">
        </tbody>
    </table>
</div>

Second, update your task_row.html to include HTMX attributes in the pagination links:

{% for task in page_obj %}
<tr>
    <td>{{ task.task }}</td>
    <td>{{ task.created_on }}</td> 
    <td>{{ task.notes }}</td> 
    <td>
        <button type="button" hx-get="{% url 'console_app:mark_task' task.pk %}" hx-target="#taskList">✔</button>
        <button type="button" hx-get="{% url 'console_app:delete_task' task.pk %}" hx-target="#taskList">DEL</button>
        <button type="button" hx-get="{% url 'console_app:edit_task' task.pk %}" hx-target="closest tr">Edit</button>
    </td>
</tr>
{% endfor %}

<tr>
    <td colspan="4">
        <div class="btn-group" role="group" aria-label="Item pagination">
            {% if page_obj.has_previous %}
                <button class="btn btn-outline-primary"
                        hx-get="{% url 'console_app:task_index' %}?page={{ page_obj.previous_page_number }}"
                        hx-target="#taskList">&laquo;</button>
            {% endif %}

            {% for page_number in page_obj.paginator.page_range %}
                {% if page_obj.number == page_number %}
                    <button class="btn btn-primary active">{{ page_number }}</button>
                {% else %}
                    <button class="btn btn-outline-primary"
                            hx-get="{% url 'console_app:task_index' %}?page={{ page_number }}"
                            hx-target="#taskList">{{ page_number }}</button>
                {% endif %}
            {% endfor %}

            {% if page_obj.has_next %}
                <button class="btn btn-outline-primary"
                        hx-get="{% url 'console_app:task_index' %}?page={{ page_obj.next_page_number }}"
                        hx-target="#taskList">&raquo;</button>
            {% endif %}
        </div>
        <div>Total record count: {{ count }}</div>
    </td>
</tr>

Third, update your view to handle both initial and HTMX requests:

def task_index(request):
    tasks = Task.objects.all().order_by(_TASK_ORDBY)   
    paginator = Paginator(tasks, 4)  
    page_number = request.GET.get('page', 1)  
    
    try:
        page_obj = paginator.page(page_number) 
    except PageNotAnInteger:
        page_obj = paginator.page(1) 
    except EmptyPage:
        page_obj = paginator.page(paginator.num_pages)

    context = {
        'page_obj': page_obj,
        'count': tasks.count()
    }

    if request.htmx:
        template = 'console_app/task_row.html'
    else:
        template = 'console_app/task_index.html'
    
    return render(request, template, context)

Fourth, I suppose that your urls.py contain something like this:

urlpatterns = [
    path('console/', console_view, name='console_view'),
    path('tasks/', task_index, name='task_index'),

]
1 Like

Thank you so much.
It really worked great. I believe it also fixes the filter issues with pagination. I will test that and update. Thank you once again.

Glad it helped! HTMX is amazing!