Pagination
Most UIs that display a list of user-created data will grow to the point that they should paginate their data.
On this page
Overview
    With the introduction of HTMX and Alpine as preferred front-end libraries in late 2024,
    and phasing out Knockout beginning in 2025, newer pages using paginated data should
    rely on HTMX + django-tables2 for handling pagination moving forward.
  
HQ has previously relied on a custom pagination component that uses Knockout Components. See pagination.js for full documentation. We will cover that example here, as it's still relevant to understand older code using this component.
Pagination with HTMX and Django Tables
    As covered in the HTMX + Alpine.JS guide,
    HTMX works by sending partial HTML responses to asynchronous requests triggered by special hx-
    attributes added to buttons, forms, links, and other elements.
  
To assist with rendering this partial HTML template, we use django-tables2 to paginate, sort, and format tabular data.
An Example
Here is an example of what this solution looks like. We will go over its components in the sections below. Please make sure to also review the comments within the code.
Table Definition
    First, we begin with the Table Definition—ExampleFakeDataTable in this example.
    It subclasses BaseHtmxTable, which inherits from django-tables2's Table.
    This object defines the table's visual style, template, and sets up the column structure and typing.
    The template and default styling are already taken care of by BaseHtmxTable.Meta,
    so most use cases just need to specify the columns when starting from BaseHtmxTable.
  
from django.utils.translation import gettext_lazy
from django_tables2 import columns
from corehq.apps.hqwebapp.tables.htmx import BaseHtmxTable
class ExampleFakeDataTable(BaseHtmxTable):
    """
    This defines the columns for the table rendered by `ExamplePaginatedTableView`.
    The variable names for each column match the keys available in
    `generate_example_pagination_data` below, and the `verbose_name` specifies
    the name shown to the user.
    We are using the `BaseHtmxTable` parent class and defining its Meta class
    below based on `BaseHtmxTable.Meta`, as it provides some shortcuts
    and default styling for our use of django-tables2 with HTMX.
    """
    class Meta(BaseHtmxTable.Meta):
        pass
    name = columns.Column(
        verbose_name=gettext_lazy("Name"),
    )
    color = columns.Column(
        verbose_name=gettext_lazy("Color"),
    )
    big_cat = columns.Column(
        verbose_name=gettext_lazy("Big Cats"),
    )
    dob = columns.Column(
        verbose_name=gettext_lazy("Date of Birth"),
    )
    app = columns.Column(
        verbose_name=gettext_lazy("Application"),
    )
    date_opened = columns.Column(
        verbose_name=gettext_lazy("Opened On"),
    )
    owner = columns.Column(
        verbose_name=gettext_lazy("Owner"),
    )
    status = columns.Column(
        verbose_name=gettext_lazy("Status"),
    )
  Table View
    Next we have the Table View, ExamplePaginatedTableView . This view renders a page of the
    ExampleFakeDataTable based on GET parameters and a queryset.
  
    Since we are using HTMX, ExamplePaginatedTableView only returns a partial template response
    that is just the table itself, page navigation, and page size selection—nothing else.
    The SelectablePaginatedTableView parent class handles page size selection and saving that choice
    in a cookie. It inherits from django-tables2's classes and mixins, which handle pagination
    within a given queryset.
  
from corehq.apps.hqwebapp.tables.pagination import SelectablePaginatedTableView
from corehq.apps.styleguide.examples.bootstrap5.htmx_pagination_data import generate_example_pagination_data
from corehq.apps.styleguide.examples.bootstrap5.htmx_pagination_table import ExampleFakeDataTable
class ExamplePaginatedTableView(SelectablePaginatedTableView):
    """
    This view returns a partial template of a table, along with its
    page controls and page size selection. Its parent classes handle
    pagination of a given queryset based on GET parameters in the request.
    This view will be fetched by the "host" `HtmxPaginationView`
    via an HTMX GET request.
    """
    urlname = "styleguide_b5_paginated_table_view"
    table_class = ExampleFakeDataTable
    def get_queryset(self):
        return generate_example_pagination_data(100)
  
    The queryset in this example is just an in-memory list of dicts for simplicity (seen below).
    However, django-tables2 also has support for Django QuerySets. We will be adding
    support for elasticsearch queries soon.
  
from corehq.apps.prototype.utils import fake_data
from corehq.util.quickcache import quickcache
@quickcache(['num_entries'])
def generate_example_pagination_data(num_entries):
    """
    This function just generates some fake data made to look somewhat like a CommCare Case.
    """
    rows = []
    for row in range(0, num_entries):
        rows.append({
            "name": f"{fake_data.get_first_name()} {fake_data.get_last_name()}",
            "color": fake_data.get_color(),
            "big_cat": fake_data.get_big_cat(),
            "dob": fake_data.get_past_date(),
            "app": fake_data.get_fake_app(),
            "status": fake_data.get_status(),
            "owner": fake_data.get_owner(),
            "date_opened": fake_data.get_past_date(months_away=[0, 1, 2, 3]),
        })
    return rows
  Host View
    Lastly, we have the host view, HtmxPaginationView. This view "hosts" the partial template
    HTML returned from HTMX requests to ExamplePaginatedTableView.
  
from django.utils.decorators import method_decorator
from django.urls import reverse
from corehq.apps.hqwebapp.decorators import use_bootstrap5
from corehq.apps.hqwebapp.views import BasePageView
@method_decorator(use_bootstrap5, name='dispatch')
class HtmxPaginationView(BasePageView):
    """
    This view hosts the Django Tables `ExamplePaginatedTableView`.
    """
    urlname = "styleguide_b5_htmx_pagination_view"
    template_name = "styleguide/bootstrap5/examples/htmx_pagination.html"
    @property
    def page_url(self):
        return reverse(self.urlname)
  
    Its template (seen below) sets up the JavaScript context and inherits from the appropriate
    CommCare HQ base template. A div makes the initial hx-get request to
    ExamplePaginatedTableView on page load (hx-trigger="load"). Subsequent
    requests are controlled by hx- attributes within the table's template.
  
{% extends "hqwebapp/bootstrap5/base_navigation.html" %}
{% load hq_shared_tags %}
{% load django_tables2 %}
{% load i18n %}
{# This is the basic entry point for pages using HTMX and Alpine that do not need additional JavaScript: #}
{% js_entry "hqwebapp/js/htmx_and_alpine" %}
{% block content %}
  <div class="container p-5">
    <h1 class="py-3 m-0">
      {% trans "Simple Pagination: HTMX + Django Tables2" %}
    </h1>
    <p>
      {% blocktrans %}
        This button will trigger a refresh on the table once its HTMX request is complete:
      {% endblocktrans %}
      <br />
      <button
        class="btn btn-primary" type="button"
        hx-get="{% url 'styleguide_a_hanging_view' %}"
        {# the attribute below triggers an `hqRefresh` event on `ExampleFakeDataTable` after the button's request is complete #}
        hq-hx-refresh-after="#ExampleFakeDataTable"
        {# we use none for `hx-swap` because the URL from `hx-get` above returns nothing in this example #}
        hx-swap="none"
      >
        <i class="fa fa-refresh"></i> {% trans "Refresh Table After Request" %}
      </button>
    </p>
    <div
      hx-trigger="load"
      {# `hx-get` asynchronously loads the table from HtmxPaginationView on page load, triggered by `hx-trigger` above #}
      {# The `{% querystring %}` template tag below is from django-tables2. It retrieves the GET parameters from the URL to properly initialize the table when they are present. #}
      hx-get="{% url "styleguide_b5_paginated_table_view" %}{% querystring %}"
    >
      <div class="htmx-indicator">
        <i class="fa-solid fa-spinner fa-spin"></i> {% trans "Loading..." %}
      </div>
    </div>
  </div>
{% endblock %}
{% block modals %}
  {# you can either include this template or include an extension of this template to show HTMX errors to the user #}
  {% include "hqwebapp/htmx/error_modal.html" %}
  {{ block.super }}
{% endblock modals %}
  
    The host view can also interact with the table from outside the original div
    that contains the partial HTML responses from ExamplePaginatedTableView. This example demonstrates
    sending a refresh event to the table using the hq-hx-refresh-after attribute placed
    on a button. After this button's own HTMX request completes, the table is reloaded.
    This example is very simple, but you can imagine chaining a refresh event to a form
    (perhaps a column filter form) that triggers table refresh after submission.
  
Using Knockout Pagination
The best way to understand the different ways of using pagination is to see its use in HQ directly. The best sources for this are the Web Users and Mobile Workers pages.
For the Web Users page, the key points are:
- the HTML widget initializing the pagination
- the goToPage javascript function that retrieves data for a given page
- the django view that returns the pagination information
An Example
Here is a quick example simulating the pagination that should otherwise be done asynchronously as in the Web Users example above. This is just so you have a visual reference of the pagination widget.
inlinePageListOnly: true to the pagination element's params. You can see an example of this on the
    HQ Dashboard page.
  <div id="js-pagination-example">
  <ul
    class="list-group"
    data-bind="foreach: items"
  >
    <li
      class="list-group-item"
      data-bind="text: $data"
    ></li>
  </ul>
  <pagination
    data-apply-bindings="false"
    params="
      goToPage: goToPage,
      slug: 'style-guide',
      perPage: perPage,
      onLoad: onPaginationLoad,
      totalItems: totalItems
    "
  ></pagination>
</div>
  import $ from 'jquery';
import ko from 'knockout';
import _ from 'underscore';
import 'hqwebapp/js/components/pagination';
$(function () {
    let paginationExample = function () {
        let self = {};
        self.items = ko.observableArray();
        self.perPage = ko.observable();
        // Most of the time the widget will only deal with a page of items at a time
        // and goToPage will be an ajax call that will fetch some items and possibly a totalItems value
        self.allItems = _.map(_.range(23), function (i) { return "Item #" + (i + 1); });
        self.totalItems = ko.observable(self.allItems.length);
        self.goToPage = function (page) {
            self.items(self.allItems.slice(self.perPage() * (page - 1), self.perPage() * page));
        };
        // Initialize with first page of data
        self.onPaginationLoad = function () {
            self.goToPage(1);
        };
        return self;
    };
    $("#js-pagination-example").koApplyBindings(paginationExample());
});