-
-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35470 from dimagi/bmb/htmx-django-tables
Add django-tables2 and utils, example, docs for usage with HTMX
- Loading branch information
Showing
33 changed files
with
958 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_loading.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} | ||
} | ||
}); |
32 changes: 32 additions & 0 deletions
32
corehq/apps/hqwebapp/static/hqwebapp/js/htmx_utils/hq_hx_refresh.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
122 changes: 122 additions & 0 deletions
122
corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 %} | ||
<div | ||
class="table-container" | ||
{% if table.container_id %} | ||
id="{{ table.container_id }}" | ||
{% endif %} | ||
{% block table-container-attrs %}{% endblock %} | ||
> | ||
|
||
{% block before_table %}{% endblock %} | ||
|
||
{% block table %}{{ block.super }}{% endblock table %} | ||
|
||
<div class="pb-3 d-flex justify-content-between"> | ||
<div> | ||
{% block num_entries %} | ||
<div class="input-group"> | ||
<div class="input-group-text"> | ||
{% 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 %} | ||
</div> | ||
{% block num_entries.select %} | ||
<select | ||
class="form-select" | ||
{% block select-per-page-attr %}{% endblock %} | ||
> | ||
{% for p in table.paginator.paging_options %} | ||
<option | ||
value="{{ p }}" | ||
{% if p == table.paginator.per_page %} selected{% endif %} | ||
> | ||
{% blocktrans %} | ||
{{ p }} per page | ||
{% endblocktrans %} | ||
</option> | ||
{% endfor %} | ||
</select> | ||
{% endblock %} | ||
</div> | ||
{% endblock num_entries %} | ||
</div> | ||
<div> | ||
{% block pagination %} | ||
{% if table.page and table.paginator.num_pages > 1 %} | ||
<nav aria-label="Table navigation"> | ||
<ul class="pagination"> | ||
|
||
{% block pagination.previous %} | ||
<li class="previous page-item{% if not table.page.has_previous %} disabled{% endif %}"> | ||
<a | ||
class="page-link" | ||
{% if table.page.has_previous %} | ||
{% block prev-page-link-attr %} | ||
href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}" | ||
{% endblock %} | ||
{% endif %} | ||
> | ||
{% trans 'Previous' %} | ||
</a> | ||
</li> | ||
{% endblock pagination.previous %} | ||
|
||
{% if table.page.has_previous or table.page.has_next %} | ||
{% block pagination.range %} | ||
{% for p in table.page|table_page_range:table.paginator %} | ||
<li class="page-item{% if table.page.number == p %} active{% endif %}"> | ||
<a | ||
class="page-link" | ||
{% if p != '...' %} | ||
href="{% querystring table.prefixed_page_field=p %}" | ||
{% endif %} | ||
> | ||
{{ p }} | ||
</a> | ||
</li> | ||
{% endfor %} | ||
{% endblock pagination.range %} | ||
{% endif %} | ||
|
||
{% block pagination.next %} | ||
<li class="next page-item{% if not table.page.has_next %} disabled{% endif %}"> | ||
<a | ||
class="page-link" | ||
{% if table.page.has_next %} | ||
{% block next-page-link-attr %} | ||
href="{% querystring table.prefixed_page_field=table.page.next_page_number %}" | ||
{% endblock %} | ||
{% endif %} | ||
> | ||
{% trans 'Next' %} | ||
</a> | ||
</li> | ||
{% endblock pagination.next %} | ||
|
||
</ul> | ||
</nav> | ||
{% endif %} | ||
{% endblock pagination %} | ||
</div> | ||
</div> | ||
|
||
{% block after_table %}{% endblock %} | ||
|
||
</div> | ||
{% endblock table-wrapper %} | ||
|
||
{% block table.thead %} | ||
{% if table.show_header %} | ||
{% render_header %} | ||
{% endif %} | ||
{% endblock table.thead %} |
Oops, something went wrong.