Skip to content

Commit

Permalink
Add django-template-partials example to Tips (#413)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Adam Johnson <me@adamj.eu>
  • Loading branch information
carltongibson and adamchainz authored Oct 25, 2024
1 parent 44b4e17 commit 2aa0fec
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 86 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Changelog

* Support Python 3.13.

* Updated :ref:`the partial rendering tip <partial-rendering>` to cover using django-template-partials.

Thanks to Carlton Gibson in `PR #413 <https://github.com/adamchainz/django-htmx/pull/413>`__.

1.19.0 (2024-08-05)
-------------------

Expand Down
82 changes: 80 additions & 2 deletions docs/tips.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,92 @@ This snippet should work with both Django templates and Jinja.

For an example of this in action, see the “CSRF Demo” page of the :doc:`example project <example_project>`.

.. _partial-rendering:

Partial Rendering
-----------------

For requests made with htmx, you may want to reduce the page content you render, since only part of the page gets updated.
This is a small optimization compared to correctly setting up compression, caching, etc.

Using django-template-partials
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The `django-template-partials package <https://github.com/carltongibson/django-template-partials>`__ extends the Django Template Language with reusable sections called “partials”.
It then allows you to render just one partial from a template.

Install ``django-template-partials`` and add its ``{% partialdef %}`` tag around a template section:

.. code-block:: django
{% extends "_base.html" %}
{% load partials %}
{% block main %}
<h1>Countries</h1>
...
{% partialdef country-table inline %}
<table id=country-data>
<thead>...</thead>
<tbody>
{% for country in countries %}
...
{% endfor %}
</tbody>
</table>
{% endpartialdef %}
...
{% endblock main %}
The above template defines a partial named ``country-table``, which renders some table of country data.
The ``inline`` argument makes the partial render when the full page renders.

In the view, you can select to render the partial for htmx requests.
This is done by adding ``#`` and the partial name to the template name:

.. code-block:: python
from django.shortcuts import render
from example.models import Country
def country_listing(request):
template_name = "countries.html"
if request.htmx:
template_name += "#country-table"
countries = Country.objects.all()
return render(
request,
template_name,
{
"countries": countries,
},
)
htmx requests will render only the partial, whilst full page requests will render the full page.
This allows refreshing of the table without an extra view or separating the template contents from its context.
For a working example, see the “Partial Rendering” page of the :doc:`example project <example_project>`.

It’s also possible to use a partial from within a separate view.
This may be preferable if other customizations are required for htmx requests.

For more information on django-template-partials, see `its documentation <https://github.com/carltongibson/django-template-partials>`__.

Swapping the base template
~~~~~~~~~~~~~~~~~~~~~~~~~~

Another technique is to swap the base template in your view.
This is a little more manual but good to have on-hand in case you need it,

You can use Django’s template inheritance to limit rendered content to only the affected section.
In your view, set up a context variable for your base template like so:

Expand Down Expand Up @@ -102,5 +182,3 @@ Here, ``_base.html`` would be the main site base:
<main id="main">
{% block main %}{% endblock %}
</main>
For an example of this in action, see the “Partial Rendering” page of the :doc:`example project <example_project>`.
1 change: 1 addition & 0 deletions example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
INSTALLED_APPS = [
"example",
"django_htmx",
"template_partials",
"django.contrib.staticfiles",
]

Expand Down
3 changes: 0 additions & 3 deletions example/example/templates/_partial.html

This file was deleted.

155 changes: 80 additions & 75 deletions example/example/templates/partial-rendering.html
Original file line number Diff line number Diff line change
@@ -1,90 +1,95 @@
{% extends base_template %}
{% extends "_base.html" %}
{% load partials %}

{% block main %}
<section>
<p>
This example shows you how you can do partial rendering for htmx requests.
The view uses an alternative base template for requests made with htmx.
This base template only renders the <code>&lt;main&gt;</code> element, saving time and bandwidth.
This example shows you how you can do partial rendering for htmx requests using <a href="https://github.com/carltongibson/django-template-partials">django-template-partials</a>.
The view renders only the content of the table section partial for requests made with htmx, saving time and bandwidth.
Paginate through the below list of randomly generated people to see this in action, and study the view and template.
</p>
<p><a href="https://django-htmx.readthedocs.io/en/latest/tips.html#partial-rendering">See more in the docs</a>.</p>
</section>

<section>
<table>
<thead>
<tr>
<th>id</th>
<th>name</th>
</tr>
</thead>
<tbody>
{% for person in page.object_list %}
<tr>
<td>{{ person.id }}</td>
<td>{{ person.name }}</td>
</tr>
{% empty %}
{% partialdef table-section inline %}
<article id=table>
<section>
<table>
<thead>
<tr>
<td colspan="2">
No people on this page.
</td>
<th>id</th>
<th>name</th>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</thead>
<tbody>
{% for person in page.object_list %}
<tr>
<td>{{ person.id }}</td>
<td>{{ person.name }}</td>
</tr>
{% empty %}
<tr>
<td colspan=2>
No people on this page.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>

<section>
<!--
The htmx attributes set on the nav here are inherited by the child links.
hx-target tells where htmx to swap the fetched content in, and hx-swap
tells it how to swap it - by replacing the 'outerHTML' attribute of the
target, i.e. replacing the target's actual DOM node. hx-push-url tells
htmx to push the fetched URL into the browser history, so we can use
the backwards/forwards buttons to navigate these subpages.
-->
<nav hx-target="#main" hx-swap="outerHTML" hx-push-url="true">
<ul>
{% if page.number != 1 %}
<li>
<!--
For each link we use hx-get to tell htmx to fetch that URL and
swap it in. We also repeat the URL in the href attribute so the
page works without JavaScript, and to ensure the link is
displayed as clickable.
-->
<a hx-get="?page=1" href="?page=1">
&laquo; First
</a>
</li>
{% endif %}
{% if page.has_previous %}
<section>
<!--
The htmx attributes set on the nav here are inherited by the child links.
hx-target tells where htmx to swap the fetched content in, and hx-swap
tells it how to swap it - by replacing the 'outerHTML' attribute of the
target, i.e. replacing the target's actual DOM node. hx-push-url tells
htmx to push the fetched URL into the browser history, so we can use
the backwards/forwards buttons to navigate these subpages.
-->
<nav hx-target=#table hx-swap=outerHTML hx-push-url=true>
<ul>
{% if page.number != 1 %}
<li>
<!--
For each link we use hx-get to tell htmx to fetch that URL and
swap it in. We also repeat the URL in the href attribute so the
page works without JavaScript, and to ensure the link is
displayed as clickable.
-->
<a hx-get="?page=1" href="?page=1">
&laquo; First
</a>
</li>
{% endif %}
{% if page.has_previous %}
<li>
<a hx-get="?page={{ page.previous_page_number }}" href="?page={{ page.previous_page_number }}">
{{ page.previous_page_number }}
</a>
</li>
{% endif %}
<li>
<a hx-get="?page={{ page.previous_page_number }}" href="?page={{ page.previous_page_number }}">
{{ page.previous_page_number }}
</a>
{{ page.number }}
</li>
{% endif %}
<li>
{{ page.number }}
</li>
{% if page.has_next %}
<li>
<a hx-get="?page={{ page.next_page_number }}" href="?page={{ page.next_page_number }}">
{{ page.next_page_number }}
</a>
</li>
{% endif %}
{% if page.number != page.paginator.num_pages %}
<li>
<a hx-get="?page={{ page.paginator.num_pages }}" href="?page={{ page.paginator.num_pages }}">
&raquo; Last
</a>
</li>
{% endif %}
</ul>
</nav>
</section>
{% if page.has_next %}
<li>
<a hx-get="?page={{ page.next_page_number }}" href="?page={{ page.next_page_number }}">
{{ page.next_page_number }}
</a>
</li>
{% endif %}
{% if page.number != page.paginator.num_pages %}
<li>
<a hx-get="?page={{ page.paginator.num_pages }}" href="?page={{ page.paginator.num_pages }}">
&raquo; Last
</a>
</li>
{% endif %}
</ul>
</nav>
</section>
</article>
{% endpartialdef %}

{% endblock main %}
10 changes: 4 additions & 6 deletions example/example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,17 @@ def partial_rendering(request: HtmxHttpRequest) -> HttpResponse:
page_num = request.GET.get("page", "1")
page = Paginator(object_list=people, per_page=10).get_page(page_num)

# The htmx magic - use a different, minimal base template for htmx
# The htmx magic - render just the `#table-section` partial for htmx
# requests, allowing us to skip rendering the unchanging parts of the
# template.
template_name = "partial-rendering.html"
if request.htmx:
base_template = "_partial.html"
else:
base_template = "_base.html"
template_name += "#table-section"

return render(
request,
"partial-rendering.html",
template_name,
{
"base_template": base_template,
"page": page,
},
)
1 change: 1 addition & 0 deletions example/requirements.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
django
django-template-partials
faker
2 changes: 2 additions & 0 deletions example/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ asgiref==3.8.1
# via django
django==5.0.7
# via -r requirements.in
django-template-partials==24.2
# via -r requirements.in
faker==26.1.0
# via -r requirements.in
python-dateutil==2.9.0.post0
Expand Down

0 comments on commit 2aa0fec

Please sign in to comment.