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'; 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/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..a8fe8b35317a --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js @@ -0,0 +1,32 @@ +/* + 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'; + +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'); +}); diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss index 0ac342feac10..6cdca1eb1257 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_tables.scss @@ -76,3 +76,95 @@ .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; + } +} + +.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; 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/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 diff --git a/corehq/apps/hqwebapp/tables/pagination.py b/corehq/apps/hqwebapp/tables/pagination.py new file mode 100644 index 000000000000..fff122e2d3ca --- /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 = getattr(self, "urlname", 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/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..3fff830829eb --- /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.container_id %}#{{ table.container_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.container_id %}#{{ table.container_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.container_id %}#{{ table.container_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 %} 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..01065be3e899 --- /dev/null +++ b/corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html @@ -0,0 +1,52 @@ +{% load querystring from django_tables2 %} + + + + {% for column in table.columns %} + + {% if column.orderable %} + {% if column.sort_desc %} + + {{ column.header }} + + {% else %} + + {{ column.header }} + + {% endif %} + {% else %} + {{ column.header }} + {% endif %} + + {% endfor %} + + 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 %} 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..38bcb6ca2a12 --- /dev/null +++ b/corehq/apps/hqwebapp/templatetags/hq_tables_tags.py @@ -0,0 +1,17 @@ +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_desc = f"-{column.name}" in sorted_columns + return context diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/includes_variables._variables.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/includes_variables._variables.style.diff.txt index 895ece9f5487..a561fe5ead42 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/includes_variables._variables.style.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/includes_variables._variables.style.diff.txt @@ -1,6 +1,6 @@ --- +++ -@@ -1,121 +1,232 @@ +@@ -1,121 +1,236 @@ -@import "@{b3-import-variables}"; - -// Nunito Sans is used on dimagi.com and embedded in hqwebapp/base.html @@ -334,3 +334,7 @@ +// 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; diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/tables._tables.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/tables._tables.style.diff.txt index abfc17b2febf..39371c370ca0 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/tables._tables.style.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/tables._tables.style.diff.txt @@ -18,3 +18,99 @@ } } } +@@ -76,3 +76,95 @@ + .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; ++ } ++} ++ ++.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/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") 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 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/htmx_and_alpine.html b/corehq/apps/styleguide/templates/styleguide/bootstrap5/htmx_and_alpine.html index b04ec00e944f..f0a9412cb4f4 100644 --- a/corehq/apps/styleguide/templates/styleguide/bootstrap5/htmx_and_alpine.html +++ b/corehq/apps/styleguide/templates/styleguide/bootstrap5/htmx_and_alpine.html @@ -259,22 +259,19 @@

    Loading Indicators

    - You can use the hx-indicator attribute - (see docs here) - to mark which element gets the htmx-request class appended to it during a request. - We've added custom styling to _htmx.scss to support the common states outlined + By default, if an element triggers an HTMX request, it will automatically get an htmx-request + CSS class applied to it. We've added custom styling to _htmx.scss to support the common states outlined later in this section.

    - By default, if an element triggers an HTMX request, it will automatically get the htmx-request - CSS class applied to it. No extra usage of the hx-indicator attribute is necessary. The - example submitting elements below showcase this default behavior without the need for - specific hx-indicator usage. + If you want to create new types of loading indicators for non-standard elements, you can explore using + the hx-indicator attribute + (see docs here).

    It's often a great idea to pair button requests with hx-disabled-elt="this" (see docs for hx-disabled-elt), - like the examples below, so that the requesting element or related element is disabled during the request. + like the examples below, so that the requesting button is disabled during the request.

    Buttons 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