From bc2bbd58e3c34326dd95da4252835aceb5097902 Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 19 Nov 2024 19:23:38 +0100 Subject: [PATCH 01/22] add django_tables2 --- requirements/base-requirements.in | 1 + requirements/dev-requirements.txt | 3 +++ requirements/docs-requirements.txt | 3 +++ requirements/prod-requirements.txt | 3 +++ requirements/requirements.txt | 3 +++ requirements/test-requirements.txt | 3 +++ settings.py | 1 + 7 files changed, 17 insertions(+) diff --git a/requirements/base-requirements.in b/requirements/base-requirements.in index baf6e21ad785..621ec1900cd3 100644 --- a/requirements/base-requirements.in +++ b/requirements/base-requirements.in @@ -35,6 +35,7 @@ django-redis-sessions django-redis django-recaptcha django-statici18n +django-tables2 django-tastypie django-transfer django-two-factor-auth @ git+https://github.com/jazzband/django-two-factor-auth.git@16f688bf329526d897a33594ab598bcd3fc8eaae # waiting for next release after 1.16.0 diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt index ad6a0aef4aab..4d14adff1268 100644 --- a/requirements/dev-requirements.txt +++ b/requirements/dev-requirements.txt @@ -166,6 +166,7 @@ django==4.2.16 # django-recaptcha # django-redis # django-statici18n + # django-tables2 # django-two-factor-auth # django-user-agents # djangorestframework @@ -223,6 +224,8 @@ django-redis-sessions==0.6.2 # via -r base-requirements.in django-statici18n==2.4.0 # via -r base-requirements.in +django-tables2==2.7.0 + # via -r base-requirements.in django-tastypie==0.14.6 # via -r base-requirements.in django-transfer==0.4 diff --git a/requirements/docs-requirements.txt b/requirements/docs-requirements.txt index 390f9c1f3c06..49c6ef2f5ac6 100644 --- a/requirements/docs-requirements.txt +++ b/requirements/docs-requirements.txt @@ -148,6 +148,7 @@ django==4.2.16 # django-recaptcha # django-redis # django-statici18n + # django-tables2 # django-two-factor-auth # django-user-agents # djangorestframework @@ -203,6 +204,8 @@ django-redis-sessions==0.6.2 # via -r base-requirements.in django-statici18n==2.4.0 # via -r base-requirements.in +django-tables2==2.7.0 + # via -r base-requirements.in django-tastypie==0.14.6 # via -r base-requirements.in django-transfer==0.4 diff --git a/requirements/prod-requirements.txt b/requirements/prod-requirements.txt index e67c6d484a17..cf1dd183bc5c 100644 --- a/requirements/prod-requirements.txt +++ b/requirements/prod-requirements.txt @@ -150,6 +150,7 @@ django==4.2.16 # django-recaptcha # django-redis # django-statici18n + # django-tables2 # django-two-factor-auth # django-user-agents # djangorestframework @@ -202,6 +203,8 @@ django-redis-sessions==0.6.2 # via -r base-requirements.in django-statici18n==2.4.0 # via -r base-requirements.in +django-tables2==2.7.0 + # via -r base-requirements.in django-tastypie==0.14.6 # via -r base-requirements.in django-transfer==0.4 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 1a2818605d13..5ba924c498ef 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -143,6 +143,7 @@ django==4.2.16 # django-recaptcha # django-redis # django-statici18n + # django-tables2 # django-two-factor-auth # django-user-agents # djangorestframework @@ -195,6 +196,8 @@ django-redis-sessions==0.6.2 # via -r base-requirements.in django-statici18n==2.4.0 # via -r base-requirements.in +django-tables2==2.7.0 + # via -r base-requirements.in django-tastypie==0.14.6 # via -r base-requirements.in django-transfer==0.4 diff --git a/requirements/test-requirements.txt b/requirements/test-requirements.txt index eb0c9b3aaeed..4fe889b5b676 100644 --- a/requirements/test-requirements.txt +++ b/requirements/test-requirements.txt @@ -150,6 +150,7 @@ django==4.2.16 # django-recaptcha # django-redis # django-statici18n + # django-tables2 # django-two-factor-auth # django-user-agents # djangorestframework @@ -202,6 +203,8 @@ django-redis-sessions==0.6.2 # via -r base-requirements.in django-statici18n==2.4.0 # via -r base-requirements.in +django-tables2==2.7.0 + # via -r base-requirements.in django-tastypie==0.14.6 # via -r base-requirements.in django-transfer==0.4 diff --git a/settings.py b/settings.py index c0336440d5ef..19974bf3944f 100755 --- a/settings.py +++ b/settings.py @@ -232,6 +232,7 @@ 'django_otp', 'django_otp.plugins.otp_static', 'django_otp.plugins.otp_totp', + 'django_tables2', 'two_factor', 'two_factor.plugins.phonenumber', 'ws4redis', From 6c3991e63c0eac588ea54340cbd3342ce0bd04fb Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 19 Nov 2024 19:28:03 +0100 Subject: [PATCH 02/22] add render_header template tag (to hq_tables_tags) this allows sortable links in headers to either work with GET querystring or htmx parameters, depending on configuration --- .../templates/hqwebapp/tables/header.html | 46 +++++++++++++++++++ .../hqwebapp/templatetags/hq_tables_tags.py | 19 ++++++++ 2 files changed, 65 insertions(+) create mode 100644 corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html create mode 100644 corehq/apps/hqwebapp/templatetags/hq_tables_tags.py diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html new file mode 100644 index 000000000000..ba75d38fb2fe --- /dev/null +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html @@ -0,0 +1,46 @@ +{% load querystring from django_tables2 %} + + + + {% for column in table.columns %} + + {% if column.orderable %} + {% if column.sort_done %} + + {{ column.header }} + + {% else %} + + {{ column.header }} + + {% endif %} + {% else %} + {{ column.header }} + {% endif %} + + {% endfor %} + + diff --git a/corehq/apps/hqwebapp/templatetags/hq_tables_tags.py b/corehq/apps/hqwebapp/templatetags/hq_tables_tags.py new file mode 100644 index 000000000000..a392cd2a6480 --- /dev/null +++ b/corehq/apps/hqwebapp/templatetags/hq_tables_tags.py @@ -0,0 +1,19 @@ +from django import template + +register = template.Library() + + +@register.inclusion_tag('hqwebapp/tables/header.html', takes_context=True) +def render_header(context, link_type=None): + """ + Based on + https://stackoverflow.com/questions/31838533/django-table2-multi-column-sorting-ui/31865765#31865765 + For allowing multiple column sorting + """ + context['use_htmx_links'] = link_type == 'htmx' + sorted_columns = context['request'].GET.getlist('sort') + for column in context['table'].columns: + column.sort_existing = column.name in sorted_columns + column.sort_done = f"-{column.name}" in sorted_columns + column.sort_new = not column.sort_existing and not column.sort_done + return context From ce5dc407c5d67c2b9f545c56a594d9e79ade9ce8 Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 19 Nov 2024 19:28:40 +0100 Subject: [PATCH 03/22] add base bootstrap5 and bootstrap5 (with HTMX) templates for django tables --- .../templates/hqwebapp/tables/bootstrap5.html | 122 ++++++++++++++++++ .../hqwebapp/tables/bootstrap5_htmx.html | 78 +++++++++++ 2 files changed, 200 insertions(+) create mode 100644 corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5.html create mode 100644 corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5.html new file mode 100644 index 000000000000..0e5ef506fff1 --- /dev/null +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5.html @@ -0,0 +1,122 @@ +{% extends 'django_tables2/bootstrap5.html' %} +{% load i18n %} +{% load django_tables2 %} +{% load hq_shared_tags %} +{% load hq_tables_tags %} + +{% block table-wrapper %} +
+ + {% block before_table %}{% endblock %} + + {% block table %}{{ block.super }}{% endblock table %} + +
+
+ {% block num_entries %} +
+
+ {% block num_entries.text %} + {% with start=table.page.start_index end=table.page.end_index total=table.page.paginator.count %} + {% blocktranslate %} + Showing {{ start }} to {{ end }} of {{ total }} entries + {% endblocktranslate %} + {% endwith %} + {% endblock num_entries.text %} +
+ {% block num_entries.select %} + + {% endblock %} +
+ {% endblock num_entries %} +
+
+ {% block pagination %} + {% if table.page and table.paginator.num_pages > 1 %} + + {% endif %} + {% endblock pagination %} +
+
+ + {% block after_table %}{% endblock %} + +
+{% endblock table-wrapper %} + +{% block table.thead %} + {% if table.show_header %} + {% render_header %} + {% endif %} +{% endblock table.thead %} diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html new file mode 100644 index 000000000000..47edd825d5dd --- /dev/null +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html @@ -0,0 +1,78 @@ +{% extends "hqwebapp/tables/bootstrap5.html" %} +{% load i18n %} +{% load django_tables2 %} +{% load hq_shared_tags %} +{% load hq_tables_tags %} + +{% block table.thead %} + {% if table.show_header %} + {% render_header 'htmx' %} + {% endif %} +{% endblock table.thead %} + +{% block table-container-attrs %} + hx-get="{{ request.path_info }}{% querystring %}" + hx-replace-url="{% querystring %}" + hx-swap="outerHTML" + hx-trigger="hqRefresh" + hq-hx-loading="{{ table.loading_indicator_id }}" +{% endblock %} + +{% block after_table %} +
+
+ {% trans "Loading..." %} +
+
+{% endblock %} + +{% block select-per-page-attr %} + name="{{ table.per_page_field }}" + hx-get="{{ request.path_info }}" + hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-swap="outerHTML" + hq-hx-loading="{{ table.loading_indicator_id }}" +{% endblock %} + +{% block prev-page-link-attr %} + hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=table.page.previous_page_number %}" + hx-replace-url="{% querystring table.prefixed_page_field=table.page.previous_page_number %}" + hx-trigger="click" + hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-swap="outerHTML" + hq-hx-loading="{{ table.loading_indicator_id }}" +{% endblock %} + +{% block next-page-link-attr %} + hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=table.page.next_page_number %}" + hx-replace-url="{% querystring table.prefixed_page_field=table.page.next_page_number %}" + hx-trigger="click" + hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-swap="outerHTML" + hq-hx-loading="{{ table.loading_indicator_id }}" +{% endblock %} + +{% block pagination.range %} + {% for p in table.page|table_page_range:table.paginator %} +
  • + + {{ p }} + +
  • + {% endfor %} +{% endblock pagination.range %} From 7739c2463a0dc003d80e4dc97c3bac6f332b911d Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 19 Nov 2024 19:29:47 +0100 Subject: [PATCH 04/22] add saved, selectable pagination support to django tables this mimics the pagination knockout component --- corehq/apps/hqwebapp/tables/__init__.py | 0 corehq/apps/hqwebapp/tables/pagination.py | 52 +++++++++++++++++++ .../hqwebapp/tables/single_table.html | 2 + 3 files changed, 54 insertions(+) create mode 100644 corehq/apps/hqwebapp/tables/__init__.py create mode 100644 corehq/apps/hqwebapp/tables/pagination.py create mode 100644 corehq/apps/hqwebapp/templates/hqwebapp/tables/single_table.html diff --git a/corehq/apps/hqwebapp/tables/__init__.py b/corehq/apps/hqwebapp/tables/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/corehq/apps/hqwebapp/tables/pagination.py b/corehq/apps/hqwebapp/tables/pagination.py new file mode 100644 index 000000000000..bd34e293a4f3 --- /dev/null +++ b/corehq/apps/hqwebapp/tables/pagination.py @@ -0,0 +1,52 @@ +from django.core.paginator import Paginator +from django.views.generic.list import ListView + +from django_tables2 import SingleTableMixin + + +class SelectablePaginator(Paginator): + paging_options = [10, 25, 50, 100] + default_option = 25 + + +class SelectablePaginatedTableMixin(SingleTableMixin): + """ + Use this mixin with django-tables2's SingleTableView + + Specify a `urlname` attribute to assist with naming the pagination cookie, + otherwise the cookie slug will default to using the class name. + """ + # `paginator_class` should always be a subclass of `SelectablePaginator` + paginator_class = SelectablePaginator + + @property + def paginate_by_cookie_slug(self): + slug = self.urlname if hasattr(self, "urlname") else self.__class__.__name__ + return f'{slug}-paginate_by' + + @property + def default_paginate_by(self): + return self.request.COOKIES.get( + self.paginate_by_cookie_slug, + self.paginator_class.default_option + ) + + @property + def current_paginate_by(self): + return self.request.GET.get('per_page', self.default_paginate_by) + + def get_paginate_by(self, table_data): + return self.current_paginate_by + + +class SelectablePaginatedTableView(SelectablePaginatedTableMixin, ListView): + """ + Based on SingleTableView, which inherits from `SingleTableMixin`, `ListView` + we instead extend the `SingleTableMixin` with `SavedPaginatedTableMixin`. + """ + template_name = "hqwebapp/tables/single_table.html" + + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + response.set_cookie(self.paginate_by_cookie_slug, self.current_paginate_by) + return response diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/single_table.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/single_table.html new file mode 100644 index 000000000000..7853b54b7664 --- /dev/null +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/single_table.html @@ -0,0 +1,2 @@ +{% load render_table from django_tables2 %} +{% render_table table %} From 89a89256d6347ae6bc066fabc8a5d496b649d4c6 Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Wed, 20 Nov 2024 18:28:25 +0100 Subject: [PATCH 05/22] add styling for sortable headers with django tables --- .../hqwebapp/scss/commcarehq/_tables.scss | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss index 0ac342feac10..5f55700292cb 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss @@ -76,3 +76,55 @@ .table-editprops-filterval { min-width: 115px; } + +.table thead tr th.orderable { + position: relative; + padding: 0; + + a { + display: block; + background-color: $blue-800; + color: $white; + padding: 0.5rem 0.5rem; + } + &:nth-child(odd) a { + background-color: $blue-700; + } + + &::before, + &::after { + position: absolute; + display: block; + right: 10px; + line-height: 9px; + font-size: .8em; + color: $white; + opacity: 0.3; + } + + &::before { + bottom: 50%; + content: "▲" / ""; + } + + &::after { + top: 50%; + content: "▼" / ""; + } + + &.asc::before { + opacity: 1.0; + } + &.desc::after { + opacity: 1.0; + } +} + +.table thead tr th.select-header { + width: 28px; + background-color: $blue-800; + + &:nth-child(odd) { + background-color: $blue-700; + } +} From cbf36e84a6e83c836cb2ace0268d844feb0bc334 Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Wed, 20 Nov 2024 18:31:34 +0100 Subject: [PATCH 06/22] trigger additional custom loading states with hq-hx-loading specifically, this adds a loading overlay to a django table which ensures nothing in the table can be interacted with until the HTMX request is complete --- .../hqwebapp/js/htmx_utils/hq_hx_loading.js | 25 ++++++++++++ .../hqwebapp/scss/commcarehq/_tables.scss | 40 +++++++++++++++++++ .../hqwebapp/scss/commcarehq/_variables.scss | 4 ++ 3 files changed, 69 insertions(+) create mode 100644 corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_loading.js diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_loading.js b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_loading.js new file mode 100644 index 000000000000..eab028396e5a --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_loading.js @@ -0,0 +1,25 @@ +/* + Adds an `is-loading` class to the element with ID specified in the `hq-hx-loading` attribute. + This `is-loading` class is applied when to that ID before an HTMX request begins, and is + removed after an HTMX swap is completed. + + This is useful for adding loading indicators to elements outside the parent heirarchy available + through using `hx-indicator` alone. Right now, this is used to add an `is-loading` style to a django tables + table, which overlays a loading indicator across the entire table (seen in hqwebapp/tables/bootstrap5_htmx.html) + */ +document.body.addEventListener('htmx:beforeRequest', (evt) => { + if (evt.detail.elt.hasAttribute('hq-hx-loading')) { + let loadingElt = document.getElementById(evt.detail.elt.getAttribute('hq-hx-loading')); + if (loadingElt) { + loadingElt.classList.add('is-loading'); + } + } +}); +document.body.addEventListener('htmx:afterSwap', (evt) => { + if (evt.detail.elt.hasAttribute('hq-hx-loading')) { + let loadingElt = document.getElementById(evt.detail.elt.getAttribute('hq-hx-loading')); + if (loadingElt && loadingElt.classList.contains('is-loading')) { + loadingElt.classList.remove('is-loading'); + } + } +}); diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss index 5f55700292cb..6cdca1eb1257 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss @@ -128,3 +128,43 @@ background-color: $blue-700; } } + +.table-container { + position: relative; + + .table-loading-indicator { + z-index: 1000; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: rgba(255, 255, 255, 0.25); + display: none; + + &.is-loading { + display: block; + } + + .spinner-border { + position: absolute; + border-width: $table-loading-spinner-border-width; + color: rgba(0, 0, 0, 0.25); + width: $table-loading-spinner-size; + height: $table-loading-spinner-size; + $_offset: $table-loading-spinner-size / 2; + left: calc(50% - $_offset); + top: calc(50% - $_offset); + } + + .table-loading-progress { + z-index: 1000; + position: absolute; + top: 0; + left: 0; + width: 100%; + background-color: $white; + font-weight: bold; + } + } +} diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_variables.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_variables.scss index 85ffb9495fb4..91cb8da2be98 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_variables.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_variables.scss @@ -230,3 +230,7 @@ $form-validation-states: ( // Make pagination-lg the same height as other lg inputs $pagination-padding-x-lg: 1.0rem; $pagination-padding-y-lg: 0.5rem; + +// Table loading indicator +$table-loading-spinner-size: 150px; +$table-loading-spinner-border-width: 20px; From 8b896721a6cd2ee6a1d427bde29d5035998e3e3b Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Thu, 21 Nov 2024 20:43:02 +0100 Subject: [PATCH 07/22] add the hq-hx-refresh HTMX plugin --- .../static/hqwebapp/js/htmx_utils/hq_hx_refresh.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js new file mode 100644 index 000000000000..9cf1f87045a6 --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js @@ -0,0 +1,10 @@ +/* + Sends an `hqRefresh` event to the selector (element) specified in the `hq-hx-refresh` attribute. + */ +import htmx from 'htmx.org'; + +document.body.addEventListener('htmx:afterSwap', (evt) => { + if (evt.detail.elt.hasAttribute('hq-hx-refresh')) { + htmx.trigger(evt.detail.elt.getAttribute('hq-hx-refresh'), 'hqRefresh'); + } +}); From 0d6eeed97d1b7b7668f7b06fa309ccfb8b86f3cb Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:55:22 +0100 Subject: [PATCH 08/22] update hq_hx_refresh to be a little clearer and add options for after request and after swap --- .../hqwebapp/js/htmx_utils/hq_hx_refresh.js | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js index 9cf1f87045a6..a8fe8b35317a 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js @@ -1,10 +1,32 @@ /* - Sends an `hqRefresh` event to the selector (element) specified in the `hq-hx-refresh` attribute. + Used to chain an `hqRefresh` event to a related HTMX element making a request. + + The attribute `hq-hx-refresh-after` sends the `hqRefresh` event to the target selector + element on `htmx:afterRequest`. + + THe attribute `hq-hx-refresh-swap` sends the `hqRefresh` event to the target selector + element on `htmx:afterSwap`. + + The value of the attributes should be a css selector--for example, `#element` where element is the CSS id. + + The target element can then apply `hx-trigger="hqRefresh"`, effectively chaining a refresh event to the + original triggering request. + + This is commonly used to trigger a refresh of tabular data with Django Tables using the `BaseHtmxTable` + subclass. However, it can be used to chain other HTMX elements together */ import htmx from 'htmx.org'; -document.body.addEventListener('htmx:afterSwap', (evt) => { - if (evt.detail.elt.hasAttribute('hq-hx-refresh')) { - htmx.trigger(evt.detail.elt.getAttribute('hq-hx-refresh'), 'hqRefresh'); +const handleRefresh = (evt, attribute) => { + if (evt.detail.elt.hasAttribute(attribute)) { + htmx.trigger(evt.detail.elt.getAttribute(attribute), 'hqRefresh'); } +}; + +document.body.addEventListener('htmx:afterRequest', (evt) => { + handleRefresh(evt, 'hq-hx-refresh-after'); +}); + +document.body.addEventListener('htmx:afterSwap', (evt) => { + handleRefresh(evt, 'hq-hx-refresh-swap'); }); From c22f69035414cc3034fef29133fc346b871b08f0 Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:56:00 +0100 Subject: [PATCH 09/22] add hq-hx loading and refresh plugins to base HTMX/Alpine entry point --- corehq/apps/hqwebapp/static/hqwebapp/js/htmx_base.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_base.js b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_base.js index 4d605f48f0dc..0f508e778092 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_base.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_base.js @@ -26,6 +26,8 @@ import htmx from 'htmx.org'; import 'hqwebapp/js/htmx_utils/hq_hx_action'; import 'hqwebapp/js/htmx_utils/csrf_token'; +import 'hqwebapp/js/htmx_utils/hq_hx_loading'; +import 'hqwebapp/js/htmx_utils/hq_hx_refresh'; import retryHtmxRequest from 'hqwebapp/js/htmx_utils/retry_request'; import { showHtmxErrorModal } from 'hqwebapp/js/htmx_utils/errors'; From 846240856bc22cbb200deb6767da1b91fd16632e Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:56:48 +0100 Subject: [PATCH 10/22] fix spacing --- .../templates/hqwebapp/tables/bootstrap5_htmx.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html index 47edd825d5dd..b5abc50ade39 100644 --- a/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html @@ -15,7 +15,7 @@ hx-replace-url="{% querystring %}" hx-swap="outerHTML" hx-trigger="hqRefresh" - hq-hx-loading="{{ table.loading_indicator_id }}" + hq-hx-loading="{{ table.loading_indicator_id }}" {% endblock %} {% block after_table %} @@ -36,7 +36,7 @@ hx-get="{{ request.path_info }}" hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" - hq-hx-loading="{{ table.loading_indicator_id }}" + hq-hx-loading="{{ table.loading_indicator_id }}" {% endblock %} {% block prev-page-link-attr %} @@ -45,7 +45,7 @@ hx-trigger="click" hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" - hq-hx-loading="{{ table.loading_indicator_id }}" + hq-hx-loading="{{ table.loading_indicator_id }}" {% endblock %} {% block next-page-link-attr %} @@ -54,7 +54,7 @@ hx-trigger="click" hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" - hq-hx-loading="{{ table.loading_indicator_id }}" + hq-hx-loading="{{ table.loading_indicator_id }}" {% endblock %} {% block pagination.range %} @@ -68,7 +68,7 @@ hx-trigger="click" hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" - hq-hx-loading="{{ table.loading_indicator_id }}" + hq-hx-loading="{{ table.loading_indicator_id }}" {% endif %} > {{ p }} From 798e0bb365b5247de5bea7bb38be439584f8338e Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:57:14 +0100 Subject: [PATCH 11/22] add more fake data generators --- corehq/apps/prototype/utils/fake_data.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/corehq/apps/prototype/utils/fake_data.py b/corehq/apps/prototype/utils/fake_data.py index 09fc083e28a8..d1cb1674b1da 100644 --- a/corehq/apps/prototype/utils/fake_data.py +++ b/corehq/apps/prototype/utils/fake_data.py @@ -39,6 +39,16 @@ def get_fake_app(): return random.choice(apps) -def get_past_date(): +def get_owner(): + owners = ('worker1', 'worker2', 'worker3', 'worker4') + return random.choice(owners) + + +def get_status(): + return random.choice(['open', 'closed']) + + +def get_past_date(months_away=None): + months_away = months_away or [6, 12, 24, 48] today = datetime.datetime.today() - return months_from_date(today, -1 * random.choice([6, 12, 24, 48])).strftime("%Y-%m-%d") + return months_from_date(today, -1 * random.choice(months_away)).strftime("%Y-%m-%d") From 9ac7666673fc72da46691d25bf1c31f4518b194e Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:57:47 +0100 Subject: [PATCH 12/22] add BaseHtmxTable class --- corehq/apps/hqwebapp/tables/htmx.py | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 corehq/apps/hqwebapp/tables/htmx.py diff --git a/corehq/apps/hqwebapp/tables/htmx.py b/corehq/apps/hqwebapp/tables/htmx.py new file mode 100644 index 000000000000..900e8d4dbe92 --- /dev/null +++ b/corehq/apps/hqwebapp/tables/htmx.py @@ -0,0 +1,50 @@ +from django_tables2 import tables + +DEFAULT_HTMX_TEMPLATE = "hqwebapp/tables/bootstrap5_htmx.html" + + +class BaseHtmxTable(tables.Table): + """ + This class provides a starting point for using HTMX with Django Tables. + + usage: + + class MyNewTable(BaseHtmxTable): + + class Meta(BaseHtmxTable.Meta): + pass # or you can override or add table attributes here -- see Django Tables docs + + Optional class properties: + + :container_id: - a string + The `container_id` is used as the css `id` of the parent div surrounding table, + its pagination, and related elements. + + You can then use `hq-hx-refresh="#{{ container_id }}"` to trigger a + refresh on the table from another HTMX request on the page. + + When not specified, `container_id` is the table's class name + + :loading_indicator_id: - a string + By default this is ":container_id:-loading". It is the css id of the table's + loading indicator that covers the whole table. The loading indicator for the table + can be triggered by any element making an HTMX request using + `hq-hx-loading="{{ loading_indicator_id }}"`. + + See `hqwebapp/tables/bootstrap5_htmx.html` and `hqwebapp/tables/bootstrap5.html` for usage of the + above properties and related `hq-hx-` attributes. There is also an example in the styleguide. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if not hasattr(self, 'container_id'): + self.container_id = self.__class__.__name__ + if not hasattr(self, 'loading_indicator_id'): + self.loading_indicator_id = f"{self.container_id}-loading" + + class Meta: + attrs = { + 'class': 'table table-striped', + } + template_name = DEFAULT_HTMX_TEMPLATE From 0ca8a1d96de0a16d4a1ddbf9fa5f447a6d53f3bc Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:58:34 +0100 Subject: [PATCH 13/22] css_id should be container_id --- .../templates/hqwebapp/tables/bootstrap5_htmx.html | 8 ++++---- .../apps/hqwebapp/templates/hqwebapp/tables/header.html | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html index b5abc50ade39..3fff830829eb 100644 --- a/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5_htmx.html @@ -34,7 +34,7 @@ {% block select-per-page-attr %} name="{{ table.per_page_field }}" hx-get="{{ request.path_info }}" - hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" hq-hx-loading="{{ table.loading_indicator_id }}" {% endblock %} @@ -43,7 +43,7 @@ hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=table.page.previous_page_number %}" hx-replace-url="{% querystring table.prefixed_page_field=table.page.previous_page_number %}" hx-trigger="click" - hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" hq-hx-loading="{{ table.loading_indicator_id }}" {% endblock %} @@ -52,7 +52,7 @@ hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=table.page.next_page_number %}" hx-replace-url="{% querystring table.prefixed_page_field=table.page.next_page_number %}" hx-trigger="click" - hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" hq-hx-loading="{{ table.loading_indicator_id }}" {% endblock %} @@ -66,7 +66,7 @@ hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=p %}" hx-replace-url="{% querystring table.prefixed_page_field=p %}" hx-trigger="click" - hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" hq-hx-loading="{{ table.loading_indicator_id }}" {% endif %} diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html index ba75d38fb2fe..2e76faf31732 100644 --- a/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html @@ -14,7 +14,7 @@ class="link" hx-get="{{ request.path_info }}{% querystring without table.prefixed_order_by_field %}" hx-replace-url="{% querystring without table.prefixed_order_by_field %}" - hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" {% else %} href="{% querystring without table.prefixed_order_by_field %}" @@ -28,7 +28,7 @@ class="link" hx-get="{{ request.path_info }}{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" hx-replace-url="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" - hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}" + hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" {% else %} href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" From ff017a7a2088a2ee3e9315edfeef461d21f2860c Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:59:01 +0100 Subject: [PATCH 14/22] make sure clicking on table headers triggers the loading indicator for table --- corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html b/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html index 2e76faf31732..aad7ea1f6bb9 100644 --- a/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html @@ -16,6 +16,9 @@ hx-replace-url="{% querystring without table.prefixed_order_by_field %}" hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" + {% if table.loading_indicator_id %} + hq-hx-loading="{{ table.loading_indicator_id }}" + {% endif %} {% else %} href="{% querystring without table.prefixed_order_by_field %}" {% endif %} @@ -30,6 +33,9 @@ hx-replace-url="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" hx-target="{% if table.container_id %}#{{ table.container_id }}{% else %}div.table-container{% endif %}" hx-swap="outerHTML" + {% if table.loading_indicator_id %} + hq-hx-loading="{{ table.loading_indicator_id }}" + {% endif %} {% else %} href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" {% endif %} From 0813758f58f460475075c31a2dd60e1ffc2bbe7a Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 21:59:48 +0100 Subject: [PATCH 15/22] update util to use fake_data from prototype --- corehq/apps/styleguide/utils.py | 39 ++++++++++----------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/corehq/apps/styleguide/utils.py b/corehq/apps/styleguide/utils.py index 9a184c124609..922321427f26 100644 --- a/corehq/apps/styleguide/utils.py +++ b/corehq/apps/styleguide/utils.py @@ -1,35 +1,20 @@ -import datetime -import random - -from corehq.apps.accounting.utils import months_from_date +from corehq.apps.prototype.utils import fake_data from corehq.util.quickcache import quickcache @quickcache(['num_entries']) def get_fake_tabular_data(num_entries): - case_owners = ('worker1', 'worker2', 'worker3', 'worker4') - status = ('open', 'closed') - first_names = ( - 'Arundhati', 'Karan', 'Salman', 'Aravind', 'Katherine', 'Ethan', 'Luna', 'Olivia', 'Stella', 'Aiden', - 'Santiago', 'Sophia', 'Parry', 'Vahan', 'Vaishnavi', 'Wambui', 'Trish', 'Prakash', - ) - last_names = ( - 'Rosalynne', 'Edwena', 'Karla', 'Zak', 'Eddy', 'Meg', 'Kelebogile', 'Monday', 'Coba', 'Zenzi', 'Rebecca', - 'Sindy', 'Earline', 'Joeri', 'Hartmann', 'Elicia', 'Marianna', 'Jonathon', 'Emilia', 'Srinivas', - ) - colors = ('blue', 'green', 'red', 'purple', 'salmon') - big_cats = ('cheetah', 'lion', 'tiger', 'panther') - apps = ('MCH V2', 'MCH V1', 'MCH V5') - today = datetime.datetime.today() rows = [] for row in range(0, num_entries): - rows.append( - ["patient", random.choice(first_names) + " " + random.choice(last_names), - random.choice(colors), - random.choice(big_cats), - months_from_date(today, -1 * random.choice([6, 12, 24, 48])).strftime("%Y-%m-%d"), - random.choice(apps), - months_from_date(today, -1 * random.choice([0, 1, 2, 3])).strftime("%Y-%m-%d"), - random.choice(case_owners), random.choice(status)] - ) + rows.append([ + "patient", + fake_data.get_first_name() + fake_data.get_last_name(), + fake_data.get_color(), + fake_data.get_big_cat(), + fake_data.get_past_date(), + fake_data.get_fake_app(), + fake_data.get_past_date(months_away=[0, 1, 2, 3]), + fake_data.get_owner(), + fake_data.get_status(), + ]) return rows From 5a80d6aad5723010d86efb38eefca3fa5fc03342 Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 22:00:49 +0100 Subject: [PATCH 16/22] add fake data for pagination example --- .../bootstrap5/htmx_pagination_data.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_data.py diff --git a/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_data.py b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_data.py new file mode 100644 index 000000000000..efb785c2c887 --- /dev/null +++ b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_data.py @@ -0,0 +1,22 @@ +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 From eb3e897e3bdfafe7054d3c16b62fdc3012c7d3e5 Mon Sep 17 00:00:00 2001 From: Biyeun Buczyk Date: Tue, 3 Dec 2024 22:01:32 +0100 Subject: [PATCH 17/22] add pagination example and documentation for HTMX + django-tables2 --- .../bootstrap5/htmx_pagination_host_view.py | 18 +++ .../bootstrap5/htmx_pagination_table.py | 45 +++++++ .../bootstrap5/htmx_pagination_table_view.py | 19 +++ .../bootstrap5/examples/htmx_pagination.html | 47 ++++++++ .../bootstrap5/molecules/pagination.html | 113 ++++++++++++++++-- corehq/apps/styleguide/urls.py | 6 + corehq/apps/styleguide/views/bootstrap5.py | 20 ++++ 7 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_host_view.py create mode 100644 corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table.py create mode 100644 corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table_view.py create mode 100644 corehq/apps/styleguide/templates/styleguide/bootstrap5/examples/htmx_pagination.html diff --git a/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_host_view.py b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_host_view.py new file mode 100644 index 000000000000..31730059393e --- /dev/null +++ b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_host_view.py @@ -0,0 +1,18 @@ +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) diff --git a/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table.py b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table.py new file mode 100644 index 000000000000..7c5d07cf9fe4 --- /dev/null +++ b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table.py @@ -0,0 +1,45 @@ +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"), + ) diff --git a/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table_view.py b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table_view.py new file mode 100644 index 000000000000..d3072f83a36a --- /dev/null +++ b/corehq/apps/styleguide/examples/bootstrap5/htmx_pagination_table_view.py @@ -0,0 +1,19 @@ +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) diff --git a/corehq/apps/styleguide/templates/styleguide/bootstrap5/examples/htmx_pagination.html b/corehq/apps/styleguide/templates/styleguide/bootstrap5/examples/htmx_pagination.html new file mode 100644 index 000000000000..1814aec09263 --- /dev/null +++ b/corehq/apps/styleguide/templates/styleguide/bootstrap5/examples/htmx_pagination.html @@ -0,0 +1,47 @@ +{% 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 %} +
    +

    + {% trans "Simple Pagination: HTMX + Django Tables2" %} +

    +

    + {% blocktrans %} + This button will trigger a refresh on the table once its HTMX request is complete: + {% endblocktrans %} +
    + +

    +
    +
    + {% trans "Loading..." %} +
    +
    +
    +{% 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 %} diff --git a/corehq/apps/styleguide/templates/styleguide/bootstrap5/molecules/pagination.html b/corehq/apps/styleguide/templates/styleguide/bootstrap5/molecules/pagination.html index 36cbdd2ecfb1..0dd90a3761dd 100644 --- a/corehq/apps/styleguide/templates/styleguide/bootstrap5/molecules/pagination.html +++ b/corehq/apps/styleguide/templates/styleguide/bootstrap5/molecules/pagination.html @@ -13,9 +13,14 @@
    On this page