Skip to content

Commit

Permalink
Add extension script with error handler
Browse files Browse the repository at this point in the history
Fixes #86.
  • Loading branch information
adamchainz committed Jul 8, 2021
1 parent f5bfc0b commit c6b95a0
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 15 deletions.
9 changes: 9 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
History
=======

* Installation now requires adding ``"django_htmx"`` to your ``INSTALLED_APPS``
setting.

* Add extension script with debug error handler. To install it, follow the new
instructions in the README.

htmx’s default behaviour is to discard error responses. The extension
overrides this in debug mode to shows Django’s debug error responses.

1.1.0 (2021-06-03)
------------------

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ prune requirements
include HISTORY.rst
include LICENSE
include README.rst
recursive-include src *.js
exclude .editorconfig
exclude .pre-commit-config.yaml
include pyproject.toml
Expand Down
72 changes: 69 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ Installation
python -m pip install django-htmx
2. Add the middleware:
2. Add django-htmx to your ``INSTALLED_APPS``:

.. code-block:: python
INSTALLED_APPS = [
...,
"django_htmx",
...,
]
3. Add the middleware:

.. code-block:: python
Expand All @@ -53,19 +63,75 @@ Installation
...,
]
4. (Optional) Add the extension script to your template, as documented below.


Example app
-----------

See the `example app <https://github.com/adamchainz/django-htmx/tree/main/example>`__ in the ``example/`` directory of the GitHub repository for usage of django-htmx.

API
---
Reference
---------

Extension Script
^^^^^^^^^^^^^^^^

django-htmx comes with a small JavaScript extension for htmx’s behaviour.
Currently the extension only includes a debug error handler, documented below.

The script is served as a static file called `django-htmx.js`, but you shouldn’t reference it directly.
Instead, use the included template tags, for both Django and Jinja templates.
You probably want to include the relevant template tag after your htmx script tag.

For **Django Templates**, load and use the template tag:

.. code-block:: django
{% load django_htmx %}
{% django_htmx_script %}
For **Jinja Templates**, you need to perform two steps.
First, load the tag function into the globals of your `custom environment <https://docs.djangoproject.com/en/stable/topics/templates/#django.template.backends.jinja2.Jinja2>`__:

.. code-block:: python
# myproject/jinja2.py
from jinja2 import Environment
from django_htmx.jinja import django_htmx_script
def environment(**options):
env = Environment(**options)
env.globals.update({
# ...
'django_htmx_script': django_htmx_script,
})
return env
Second, call the function in your base template:

.. code-block:: jinja2
{{ django_htmx_script() }}
Debug Error Handler
~~~~~~~~~~~~~~~~~~~

htmx’s default behaviour when encountering a server error is to discard the response.
This can make it hard to debug errors in development.
The django-htmx script includes an error handler that’s active when debug mode is on.
This detects error responses and replaces the page with their content, allowing you to debug with Django’s default error responses as you would for a non-htmx request.

See this in action in the “Error Demo” section of the example app.

``django_htmx.middleware.HtmxMiddleware``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This middleware attaches ``request.htmx``, an instance of ``HtmxDetails``.

See it action in the “Middleware Tester” section of the example app.

``django_htmx.middleware.HtmxDetails``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
13 changes: 13 additions & 0 deletions example/example/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ def csrf_demo_checker(request):
)


# Error demo


@require_http_methods(("GET",))
def error_demo(request):
return render(request, "error-demo.html")


@require_http_methods(("GET",))
def error_demo_trigger(request):
1 / 0


# Middleware tester

# This uses two views - one to render the form, and the second to render the
Expand Down
2 changes: 1 addition & 1 deletion example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

INSTALLED_APPS = [
"example.core",
"django_htmx",
"django.contrib.staticfiles",
]

Expand All @@ -31,7 +32,6 @@
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
]
},
Expand Down
15 changes: 11 additions & 4 deletions example/example/templates/_base.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{% load django_htmx %}
{% load static %}

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>django-htmx example app</title>
<link rel="stylesheet" href="/static/mvp.css">
<link rel="stylesheet" href="{% static 'mvp.css' %}">

<!-- An example of how we can configure htmx -->
<!-- https://htmx.org/docs/#config -->
Expand All @@ -27,6 +30,9 @@ <h1>
<li>
<a href="/csrf-demo/">CSRF Demo</a>
</li>
<li>
<a href="/error-demo/">Error Demo</a>
</li>
<li>
<a href="/middleware-tester/">Middleware Tester</a>
</li>
Expand All @@ -45,8 +51,9 @@ <h1>
For more information see <a href="https://github.com/adamchainz/django-htmx">django-htmx</a> and <a href="https://htmx.org/">htmx</a>.
</p>
</footer>
<script src="/static/htmx.min.js"></script>
<script src="/static/ext/debug.js"></script>
<script src="/static/ext/event-header.js"></script>
<script src="{% static 'htmx.min.js' %}"></script>
<script src="{% static 'ext/debug.js' %}"></script>
<script src="{% static 'ext/event-header.js' %}"></script>
{% django_htmx_script %}
</body>
</html>
23 changes: 23 additions & 0 deletions example/example/templates/error-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "_base.html" %}

{% block main %}
<section>
<p>
This page shows you django-htmx’s error handler.
</p>
</section>
<section>
<p>
{% if DEBUG %}
The error handler will work, since <code>DEBUG = True</code>.
{% else %}
The error handler will <strong>not</strong> work, since <strong>DEBUG = False</strong>.
{% endif %}
</p>
</section>
<section>
<button hx-get="/error-demo/trigger/">
Try divide by zero
</button>
</section>
{% endblock %}
4 changes: 4 additions & 0 deletions example/example/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from example.core.views import (
csrf_demo,
csrf_demo_checker,
error_demo,
error_demo_trigger,
index,
middleware_tester,
middleware_tester_table,
Expand All @@ -13,6 +15,8 @@
path("", index),
path("csrf-demo/", csrf_demo),
path("csrf-demo/checker/", csrf_demo_checker),
path("error-demo/", error_demo),
path("error-demo/trigger/", error_demo_trigger),
path("middleware-tester/", middleware_tester),
path("middleware-tester/table/", middleware_tester_table),
path("partial-rendering/", partial_rendering),
Expand Down
8 changes: 4 additions & 4 deletions example/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#
# This file is autogenerated by pip-compile
# This file is autogenerated by pip-compile with python 3.9
# To update, run:
#
# pip-compile
#
asgiref==3.3.4
asgiref==3.4.1
# via django
django==3.2.4
django==3.2.5
# via -r requirements.in
faker==8.5.0
faker==8.9.1
# via -r requirements.in
python-dateutil==2.8.1
# via faker
Expand Down
15 changes: 15 additions & 0 deletions src/django_htmx/jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.conf import settings
from django.templatetags.static import static
from django.utils.html import format_html


def django_htmx_script():
# Optimization: whilst the script has no behaviour outside of debug mode,
# don't include it.
if not settings.DEBUG:
return format_html("")
return format_html(
'<script type="text/javascript" src="{}" data-debug="{}" async defer></script>',
static("django-htmx.js"),
str(bool(settings.DEBUG)),
)
22 changes: 22 additions & 0 deletions src/django_htmx/static/django-htmx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
const data = document.currentScript.dataset;
const isDebug = data.debug === "True";

if (isDebug) {
document.addEventListener("htmx:beforeOnLoad", function (event) {
const xhr = event.detail.xhr;
if (xhr.status == 500) {
// Tell htmx to stop processing this response
event.stopPropagation();

document.children[0].innerHTML = xhr.response;

// Run Django’s inline script
// (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript
(1, eval)(document.scripts[0].innerText);
// Need to directly call Django’s onload function since browser won’t
window.onload();
}
});
}
}
Empty file.
6 changes: 6 additions & 0 deletions src/django_htmx/templatetags/django_htmx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django import template

from django_htmx.jinja import django_htmx_script

register = template.Library()
register.simple_tag(django_htmx_script)
16 changes: 13 additions & 3 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
SECRET_KEY = "NOTASECRET"

DATABASES = {}

ALLOWED_HOSTS = []

INSTALLED_APPS = []
DATABASES = {}

INSTALLED_APPS = [
"django_htmx",
]

MIDDLEWARE = []

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"OPTIONS": {"context_processors": []},
}
]
Empty file added tests/templatetags/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions tests/templatetags/test_django_htmx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.template import Context, Template
from django.test import SimpleTestCase, override_settings


class DjangoHtmxScriptTests(SimpleTestCase):
def test_non_debug_empty(self):
result = Template("{% load django_htmx %}{% django_htmx_script %}").render(
Context()
)

assert result == ""

def test_debug_success(self):
with override_settings(DEBUG=True):
result = Template("{% load django_htmx %}{% django_htmx_script %}").render(
Context()
)

assert result == (
'<script type="text/javascript" src="django-htmx.js" '
+ 'data-debug="True" async defer></script>'
)
19 changes: 19 additions & 0 deletions tests/test_jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.test import SimpleTestCase, override_settings

from django_htmx.jinja import django_htmx_script


class DjangoHtmxScriptTests(SimpleTestCase):
def test_non_debug_empty(self):
result = django_htmx_script()

assert result == ""

def test_debug_success(self):
with override_settings(DEBUG=True):
result = django_htmx_script()

assert result == (
'<script type="text/javascript" src="django-htmx.js" '
+ 'data-debug="True" async defer></script>'
)

0 comments on commit c6b95a0

Please sign in to comment.