plain.htmx 0.15.0__tar.gz → 0.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/PKG-INFO +115 -105
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/CHANGELOG.md +20 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/README.md +114 -104
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/templates.py +3 -3
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/pyproject.toml +1 -1
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/.gitignore +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/LICENSE +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/README.md +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/deps.yml +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/package-lock.json +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/package.json +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/__init__.py +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/plainhtmx.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-ext.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-ext.min.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-htmx.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.amd.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.cjs.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.esm.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.min.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/README.md +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/ajax-header.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/alpine-morph.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/class-tools.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/client-side-templates.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/debug.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/disable-element.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/event-header.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/head-support.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/include-vals.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/json-enc.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/loading-states.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/method-override.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/morphdom-swap.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/multi-swap.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/path-deps.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/path-params.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/preload.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/rails-method.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/remove-me.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/response-targets.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/restored.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/sse.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/ws.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/htmx.amd.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/htmx.cjs.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/htmx.esm.d.ts +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/htmx.esm.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/htmx.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/htmx.min.js +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/htmx.min.js.gz +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/templates/htmx/js.html +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/views.py +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/tests/app/settings.py +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/tests/app/urls.py +0 -0
- {plain_htmx-0.15.0 → plain_htmx-0.16.0}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.htmx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: Integrate HTMX with templates and views.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -14,43 +14,35 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
**Integrate HTMX with templates and views.**
|
|
15
15
|
|
|
16
16
|
- [Overview](#overview)
|
|
17
|
-
- [Template
|
|
17
|
+
- [Template fragments](#template-fragments)
|
|
18
18
|
- [Lazy template fragments](#lazy-template-fragments)
|
|
19
|
-
- [How
|
|
20
|
-
- [View
|
|
21
|
-
- [Dedicated
|
|
22
|
-
- [
|
|
23
|
-
- [CSRF tokens](#csrf-tokens)
|
|
24
|
-
- [Error classes](#error-classes)
|
|
25
|
-
- [CSP](#csp)
|
|
19
|
+
- [How template fragments work](#how-template-fragments-work)
|
|
20
|
+
- [View actions](#view-actions)
|
|
21
|
+
- [Dedicated templates](#dedicated-templates)
|
|
22
|
+
- [FAQs](#faqs)
|
|
26
23
|
- [Installation](#installation)
|
|
27
24
|
|
|
28
25
|
## Overview
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
One is [template fragments](#template-fragments) and the other is [view actions](#view-actions).
|
|
27
|
+
You can use `plain.htmx` to build HTMX-powered views that focus on server-side rendering without needing complicated URL structures or REST APIs.
|
|
32
28
|
|
|
33
|
-
The
|
|
29
|
+
The two main features are [template fragments](#template-fragments) and [view actions](#view-actions).
|
|
34
30
|
|
|
35
|
-
The [`
|
|
36
|
-
To use these features on a view,
|
|
37
|
-
simply inherit from the class (yes, this is designed to work with class-based views).
|
|
31
|
+
The [`HTMXView`](./views.py#HTMXView) class is the starting point for the server-side HTMX behavior. To use these features on a view, inherit from this class (yes, this is designed to work with class-based views).
|
|
38
32
|
|
|
39
33
|
```python
|
|
40
|
-
|
|
34
|
+
# app/views.py
|
|
35
|
+
from plain.htmx.views import HTMXView
|
|
41
36
|
|
|
42
|
-
from plain.htmx.views import HTMXViewMixin
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
class HomeView(HTMXViewMixin, TemplateView):
|
|
38
|
+
class HomeView(HTMXView):
|
|
46
39
|
template_name = "home.html"
|
|
47
40
|
```
|
|
48
41
|
|
|
49
|
-
In your `base.html` template (or wherever need the HTMX scripts),
|
|
50
|
-
you can use the `{% htmx_js %}` template tag:
|
|
42
|
+
In your `base.html` template (or wherever you need the HTMX scripts), you can use the `{% htmx_js %}` template tag:
|
|
51
43
|
|
|
52
44
|
```html
|
|
53
|
-
<!-- base.
|
|
45
|
+
<!-- base.html -->
|
|
54
46
|
{% load htmx %}
|
|
55
47
|
<!DOCTYPE html>
|
|
56
48
|
<html lang="en">
|
|
@@ -64,12 +56,9 @@ you can use the `{% htmx_js %}` template tag:
|
|
|
64
56
|
</body>
|
|
65
57
|
```
|
|
66
58
|
|
|
67
|
-
## Template
|
|
59
|
+
## Template fragments
|
|
68
60
|
|
|
69
|
-
An `{% htmxfragment %}` can
|
|
70
|
-
When you use a fragment, all `hx-get`, `hx-post`, etc. elements inside that fragment will automatically send a request to the current URL,
|
|
71
|
-
render _only_ the updated content for the fragment,
|
|
72
|
-
and swap out the fragment.
|
|
61
|
+
An `{% htmxfragment %}` can render a specific part of your template in HTMX responses. When you use a fragment, all `hx-get`, `hx-post`, etc. elements inside that fragment will automatically send a request to the current URL, render _only_ the updated content for the fragment, and swap out the fragment.
|
|
73
62
|
|
|
74
63
|
Here's an example:
|
|
75
64
|
|
|
@@ -98,8 +87,7 @@ Everything inside `{% htmxfragment %}` will automatically update when "Refresh"
|
|
|
98
87
|
|
|
99
88
|
### Lazy template fragments
|
|
100
89
|
|
|
101
|
-
If you want to render a fragment lazily,
|
|
102
|
-
you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
|
|
90
|
+
If you want to render a fragment lazily, you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
|
|
103
91
|
|
|
104
92
|
```html
|
|
105
93
|
{% htmxfragment "main" lazy=True %}
|
|
@@ -107,8 +95,7 @@ you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
|
|
|
107
95
|
{% endhtmxfragment %}
|
|
108
96
|
```
|
|
109
97
|
|
|
110
|
-
This pairs nicely with passing a callable function or method as a context variable,
|
|
111
|
-
which will only get invoked when the fragment actually gets rendered on the lazy load.
|
|
98
|
+
This pairs nicely with passing a callable function or method as a context variable, which will only get invoked when the fragment actually gets rendered on the lazy load.
|
|
112
99
|
|
|
113
100
|
```python
|
|
114
101
|
def fetch_items():
|
|
@@ -117,9 +104,9 @@ def fetch_items():
|
|
|
117
104
|
return ["foo", "bar", "baz"]
|
|
118
105
|
|
|
119
106
|
|
|
120
|
-
class HomeView(
|
|
121
|
-
def
|
|
122
|
-
context = super().
|
|
107
|
+
class HomeView(HTMXView):
|
|
108
|
+
def get_template_context(self):
|
|
109
|
+
context = super().get_template_context()
|
|
123
110
|
context["items"] = fetch_items # Missing () are on purpose!
|
|
124
111
|
return context
|
|
125
112
|
```
|
|
@@ -134,10 +121,9 @@ class HomeView(HTMXViewMixin, TemplateView):
|
|
|
134
121
|
{% endhtmxfragment %}
|
|
135
122
|
```
|
|
136
123
|
|
|
137
|
-
|
|
124
|
+
### How template fragments work
|
|
138
125
|
|
|
139
|
-
When you use the `{% htmxfragment %}` tag,
|
|
140
|
-
a standard `div` is output that looks like this:
|
|
126
|
+
When you use the `{% htmxfragment %}` tag, a standard `div` is output that looks like this:
|
|
141
127
|
|
|
142
128
|
```html
|
|
143
129
|
<div plain-hx-fragment="main" hx-swap="outerHTML" hx-target="this" hx-indicator="this">
|
|
@@ -145,39 +131,25 @@ a standard `div` is output that looks like this:
|
|
|
145
131
|
</div>
|
|
146
132
|
```
|
|
147
133
|
|
|
148
|
-
The `plain-hx-fragment` is a custom attribute
|
|
149
|
-
but the rest are standard HTMX attributes.
|
|
134
|
+
The `plain-hx-fragment` is a custom attribute, but the rest are standard HTMX attributes.
|
|
150
135
|
|
|
151
|
-
When Plain renders the response to an HTMX request,
|
|
152
|
-
it will get the `Plain-HX-Fragment` header,
|
|
153
|
-
find the fragment with that name in the template,
|
|
154
|
-
and render that for the response.
|
|
136
|
+
When Plain renders the response to an HTMX request, it will get the `Plain-HX-Fragment` header, find the fragment with that name in the template, and render that for the response.
|
|
155
137
|
|
|
156
138
|
Then the response content is automatically swapped in to replace the content of your `{% htmxfragment %}` tag.
|
|
157
139
|
|
|
158
|
-
Note that there is no URL specified on the `hx-get` attribute.
|
|
159
|
-
By default, HTMX will send the request to the current URL for the page.
|
|
160
|
-
When you're working with fragments, this is typically the behavior you want!
|
|
161
|
-
(You're on a page and want to selectively re-render a part of that page.)
|
|
140
|
+
Note that there is no URL specified on the `hx-get` attribute. By default, HTMX will send the request to the current URL for the page. When you're working with fragments, this is typically the behavior you want! (You're on a page and want to selectively re-render a part of that page.)
|
|
162
141
|
|
|
163
|
-
The `{% htmxfragment %}` tag is somewhat similar to a `{% block %}` tag --
|
|
164
|
-
the fragments on a page should be named and unique,
|
|
165
|
-
and you can't use it inside of loops.
|
|
166
|
-
For fragment-like behavior inside of a for-loop,
|
|
167
|
-
you'll most likely want to set up a dedicated URL that can handle a single instance of the looped items,
|
|
168
|
-
and maybe leverage [dedicated templates](#dedicated-templates).
|
|
142
|
+
The `{% htmxfragment %}` tag is somewhat similar to a `{% block %}` tag -- the fragments on a page should be named and unique, and you can't use it inside of loops. For fragment-like behavior inside of a for-loop, you'll most likely want to set up a dedicated URL that can handle a single instance of the looped items, and maybe leverage [dedicated templates](#dedicated-templates).
|
|
169
143
|
|
|
170
|
-
## View
|
|
144
|
+
## View actions
|
|
171
145
|
|
|
172
|
-
View actions let you define multiple "actions" on a class-based view.
|
|
173
|
-
This is an alternative to defining specific API endpoints or form views to handle basic button interactions.
|
|
146
|
+
View actions let you define multiple "actions" on a class-based view. This is an alternative to defining specific API endpoints or form views to handle basic button interactions.
|
|
174
147
|
|
|
175
|
-
With view actions you can design a single view that renders a single template,
|
|
176
|
-
and associate buttons in that template with class methods in the view.
|
|
148
|
+
With view actions you can design a single view that renders a single template, and associate buttons in that template with class methods in the view.
|
|
177
149
|
|
|
178
150
|
As an example, let's say we have a `PullRequest` model and we want users to be able to open, close, or merge it with a button.
|
|
179
151
|
|
|
180
|
-
In
|
|
152
|
+
In your template, use the `plain-hx-action` attribute to name the action:
|
|
181
153
|
|
|
182
154
|
```html
|
|
183
155
|
{% extends "base.html" %}
|
|
@@ -207,10 +179,14 @@ In our template, we would use the `plain-hx-action` attribute to name the action
|
|
|
207
179
|
{% endblock %}
|
|
208
180
|
```
|
|
209
181
|
|
|
210
|
-
Then in the view class,
|
|
182
|
+
Then in the view class, define methods for each HTTP method + `plain-hx-action`:
|
|
211
183
|
|
|
212
184
|
```python
|
|
213
|
-
|
|
185
|
+
from plain.htmx.views import HTMXView
|
|
186
|
+
from plain.views import DetailView
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PullRequestDetailView(HTMXView, DetailView):
|
|
214
190
|
def get_queryset(self):
|
|
215
191
|
# The queryset will apply to all actions on the view, so "permission" logic can be shared
|
|
216
192
|
return super().get_queryset().filter(users=self.user)
|
|
@@ -224,44 +200,39 @@ class PullRequestDetailView(HTMXViewMixin, DetailView):
|
|
|
224
200
|
self.object.state = "closed"
|
|
225
201
|
self.object.save()
|
|
226
202
|
|
|
227
|
-
# Render the updated content the standard calls
|
|
228
|
-
# (which will selectively render
|
|
229
|
-
|
|
230
|
-
return self.render_to_response(context)
|
|
203
|
+
# Render the updated content with the standard calls
|
|
204
|
+
# (which will selectively render the fragment if applicable)
|
|
205
|
+
return self.render_template()
|
|
231
206
|
|
|
232
207
|
def htmx_post_close(self):
|
|
233
208
|
if self.object.state != "open":
|
|
234
|
-
raise ValueError("Only
|
|
209
|
+
raise ValueError("Only an open pull request can be closed")
|
|
235
210
|
|
|
236
211
|
self.object.state = "open"
|
|
237
212
|
self.object.save()
|
|
238
213
|
|
|
239
|
-
|
|
240
|
-
return self.render_to_response(context)
|
|
214
|
+
return self.render_template()
|
|
241
215
|
|
|
242
216
|
def htmx_post_merge(self):
|
|
243
217
|
if self.object.state != "open":
|
|
244
|
-
raise ValueError("Only
|
|
218
|
+
raise ValueError("Only an open pull request can be merged")
|
|
245
219
|
|
|
246
220
|
self.object.state = "merged"
|
|
247
221
|
self.object.save()
|
|
248
222
|
|
|
249
|
-
|
|
250
|
-
return self.render_to_response(context)
|
|
223
|
+
return self.render_template()
|
|
251
224
|
```
|
|
252
225
|
|
|
253
|
-
This can be a matter of preference,
|
|
254
|
-
but typically you may end up building out an entire form, API, or set of URLs to handle these behaviors.
|
|
255
|
-
If you application is only going to handle these actions via HTMX,
|
|
256
|
-
then a single View may be a simpler way to do it.
|
|
226
|
+
This can be a matter of preference, but typically you may end up building out an entire form, API, or set of URLs to handle these behaviors. If your application is only going to handle these actions via HTMX, then a single View may be a simpler way to do it.
|
|
257
227
|
|
|
258
|
-
|
|
259
|
-
this can basically all be done through standard request and response headers:
|
|
228
|
+
You can also handle HTMX requests without a specific action by just implementing the HTTP method:
|
|
260
229
|
|
|
261
230
|
```python
|
|
262
|
-
|
|
231
|
+
from plain.http import HttpResponse
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class PullRequestDetailView(HTMXView, DetailView):
|
|
263
235
|
def get_queryset(self):
|
|
264
|
-
# The queryset will apply to all actions on the view, so "permission" logic can be shared
|
|
265
236
|
return super().get_queryset().filter(users=self.user)
|
|
266
237
|
|
|
267
238
|
# You can also leave off the "plain-hx-action" attribute and just handle the HTTP method
|
|
@@ -274,18 +245,13 @@ class PullRequestDetailView(HTMXViewMixin, DetailView):
|
|
|
274
245
|
return response
|
|
275
246
|
```
|
|
276
247
|
|
|
277
|
-
## Dedicated
|
|
248
|
+
## Dedicated templates
|
|
278
249
|
|
|
279
|
-
A small additional
|
|
280
|
-
More than anything, this is just a nice way to formalize a naming scheme for template "partials" dedicated to HTMX.
|
|
250
|
+
A small additional feature is that `plain.htmx` will automatically find templates named `{template_name}_htmx.html` for HTMX requests. More than anything, this is just a nice way to formalize a naming scheme for template "partials" dedicated to HTMX.
|
|
281
251
|
|
|
282
|
-
Because template fragments don't work inside of loops,
|
|
283
|
-
for example,
|
|
284
|
-
you'll often need to define dedicated URLs to handle the HTMX behaviors for individual items in a loop.
|
|
285
|
-
You can sometimes think of these as "pages within a page".
|
|
252
|
+
Because template fragments don't work inside of loops, for example, you'll often need to define dedicated URLs to handle the HTMX behaviors for individual items in a loop. You can sometimes think of these as "pages within a page".
|
|
286
253
|
|
|
287
|
-
So if you have a template that renders a collection of items,
|
|
288
|
-
you can do the initial render using a Django `{% include %}`:
|
|
254
|
+
So if you have a template that renders a collection of items, you can do the initial render using a `{% include %}`:
|
|
289
255
|
|
|
290
256
|
```html
|
|
291
257
|
<!-- pullrequests/pullrequest_list.html -->
|
|
@@ -337,21 +303,20 @@ urlpatterns = [
|
|
|
337
303
|
]
|
|
338
304
|
|
|
339
305
|
# views.py
|
|
340
|
-
class PullRequestDetailView(
|
|
306
|
+
class PullRequestDetailView(HTMXView, DetailView):
|
|
341
307
|
def htmx_post_update(self):
|
|
342
308
|
self.object.update()
|
|
343
309
|
|
|
344
|
-
|
|
345
|
-
return self.render_to_response(context)
|
|
310
|
+
return self.render_template()
|
|
346
311
|
```
|
|
347
312
|
|
|
348
|
-
##
|
|
313
|
+
## FAQs
|
|
349
314
|
|
|
350
|
-
|
|
351
|
-
This tells HTMX to add the `htmx-request` class to the fragment element when it is loading.
|
|
315
|
+
#### How do I add a Tailwind CSS variant for loading states?
|
|
352
316
|
|
|
353
|
-
|
|
354
|
-
|
|
317
|
+
The standard behavior for `{% htmxfragment %}` is to set `hx-indicator="this"` on the rendered element. This tells HTMX to add the `htmx-request` class to the fragment element when it is loading.
|
|
318
|
+
|
|
319
|
+
Here's a simple variant you can add to your `tailwind.config.js` to easily style the loading state:
|
|
355
320
|
|
|
356
321
|
```js
|
|
357
322
|
const plugin = require('tailwindcss/plugin')
|
|
@@ -379,14 +344,13 @@ You can then prefix any class with `htmx-request:` to decide what it looks like
|
|
|
379
344
|
</form>
|
|
380
345
|
```
|
|
381
346
|
|
|
382
|
-
|
|
347
|
+
#### How are CSRF tokens handled?
|
|
383
348
|
|
|
384
|
-
|
|
385
|
-
You don't have to put `hx-headers` on the `<body>` tag, for example.
|
|
349
|
+
CSRF tokens are configured automatically with the HTMX JS API. You don't have to put `hx-headers` on the `<body>` tag.
|
|
386
350
|
|
|
387
|
-
|
|
351
|
+
#### How do I show error states?
|
|
388
352
|
|
|
389
|
-
This
|
|
353
|
+
This package includes an HTMX extension for adding error classes for failed requests:
|
|
390
354
|
|
|
391
355
|
- `htmx-error-response` for `htmx:responseError`
|
|
392
356
|
- `htmx-error-response-{{ status_code }}` for `htmx:responseError`
|
|
@@ -394,7 +358,7 @@ This app also includes an HTMX extension for adding error classes for failed req
|
|
|
394
358
|
|
|
395
359
|
To enable them, use `hx-ext="plain-errors"`.
|
|
396
360
|
|
|
397
|
-
You can add the ones you want as Tailwind variants and use them to show error messages
|
|
361
|
+
You can add the ones you want as Tailwind variants and use them to show error messages:
|
|
398
362
|
|
|
399
363
|
```js
|
|
400
364
|
const plugin = require('tailwindcss/plugin')
|
|
@@ -407,9 +371,11 @@ module.exports = {
|
|
|
407
371
|
}
|
|
408
372
|
```
|
|
409
373
|
|
|
410
|
-
|
|
374
|
+
#### How do I configure HTMX for CSP?
|
|
411
375
|
|
|
412
|
-
|
|
376
|
+
If you're using Content Security Policy, you can disable the indicator styles that HTMX adds inline:
|
|
377
|
+
|
|
378
|
+
```html
|
|
413
379
|
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
|
|
414
380
|
```
|
|
415
381
|
|
|
@@ -421,11 +387,55 @@ Install the `plain.htmx` package from [PyPI](https://pypi.org/project/plain.htmx
|
|
|
421
387
|
uv add plain.htmx
|
|
422
388
|
```
|
|
423
389
|
|
|
424
|
-
|
|
390
|
+
Add `plain.htmx` to your installed packages:
|
|
425
391
|
|
|
426
392
|
```python
|
|
393
|
+
# app/settings.py
|
|
427
394
|
INSTALLED_PACKAGES = [
|
|
428
395
|
# ...
|
|
429
396
|
"plain.htmx",
|
|
430
397
|
]
|
|
431
398
|
```
|
|
399
|
+
|
|
400
|
+
Add the HTMX JavaScript to your base template:
|
|
401
|
+
|
|
402
|
+
```html
|
|
403
|
+
<!-- base.html -->
|
|
404
|
+
{% load htmx %}
|
|
405
|
+
<!DOCTYPE html>
|
|
406
|
+
<html lang="en">
|
|
407
|
+
<head>
|
|
408
|
+
<meta charset="UTF-8">
|
|
409
|
+
<title>My Site</title>
|
|
410
|
+
{% htmx_js %}
|
|
411
|
+
</head>
|
|
412
|
+
<body>
|
|
413
|
+
{% block content %}{% endblock %}
|
|
414
|
+
</body>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Create a view that inherits from `HTMXView`:
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
# app/views.py
|
|
421
|
+
from plain.htmx.views import HTMXView
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class HomeView(HTMXView):
|
|
425
|
+
template_name = "home.html"
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Create a template with an HTMX fragment:
|
|
429
|
+
|
|
430
|
+
```html
|
|
431
|
+
<!-- home.html -->
|
|
432
|
+
{% extends "base.html" %}
|
|
433
|
+
{% load htmx %}
|
|
434
|
+
|
|
435
|
+
{% block content %}
|
|
436
|
+
{% htmxfragment "content" %}
|
|
437
|
+
<p>The time is {% now "jS F Y H:i" %}</p>
|
|
438
|
+
<button hx-get>Refresh</button>
|
|
439
|
+
{% endhtmxfragment %}
|
|
440
|
+
{% endblock %}
|
|
441
|
+
```
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# plain-htmx changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.0](https://github.com/dropseed/plain/releases/plain-htmx@0.16.0) (2026-01-13)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Improved README documentation with better structure, examples, and FAQs section ([da37a78](https://github.com/dropseed/plain/commit/da37a78))
|
|
8
|
+
|
|
9
|
+
### Upgrade instructions
|
|
10
|
+
|
|
11
|
+
- No changes required
|
|
12
|
+
|
|
13
|
+
## [0.15.1](https://github.com/dropseed/plain/releases/plain-htmx@0.15.1) (2025-12-22)
|
|
14
|
+
|
|
15
|
+
### What's changed
|
|
16
|
+
|
|
17
|
+
- Internal code cleanup to remove unnecessary type ignore comments ([539a706](https://github.com/dropseed/plain/commit/539a706))
|
|
18
|
+
|
|
19
|
+
### Upgrade instructions
|
|
20
|
+
|
|
21
|
+
- No changes required
|
|
22
|
+
|
|
3
23
|
## [0.15.0](https://github.com/dropseed/plain/releases/plain-htmx@0.15.0) (2025-12-09)
|
|
4
24
|
|
|
5
25
|
### What's changed
|
|
@@ -3,43 +3,35 @@
|
|
|
3
3
|
**Integrate HTMX with templates and views.**
|
|
4
4
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
|
-
- [Template
|
|
6
|
+
- [Template fragments](#template-fragments)
|
|
7
7
|
- [Lazy template fragments](#lazy-template-fragments)
|
|
8
|
-
- [How
|
|
9
|
-
- [View
|
|
10
|
-
- [Dedicated
|
|
11
|
-
- [
|
|
12
|
-
- [CSRF tokens](#csrf-tokens)
|
|
13
|
-
- [Error classes](#error-classes)
|
|
14
|
-
- [CSP](#csp)
|
|
8
|
+
- [How template fragments work](#how-template-fragments-work)
|
|
9
|
+
- [View actions](#view-actions)
|
|
10
|
+
- [Dedicated templates](#dedicated-templates)
|
|
11
|
+
- [FAQs](#faqs)
|
|
15
12
|
- [Installation](#installation)
|
|
16
13
|
|
|
17
14
|
## Overview
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
One is [template fragments](#template-fragments) and the other is [view actions](#view-actions).
|
|
16
|
+
You can use `plain.htmx` to build HTMX-powered views that focus on server-side rendering without needing complicated URL structures or REST APIs.
|
|
21
17
|
|
|
22
|
-
The
|
|
18
|
+
The two main features are [template fragments](#template-fragments) and [view actions](#view-actions).
|
|
23
19
|
|
|
24
|
-
The [`
|
|
25
|
-
To use these features on a view,
|
|
26
|
-
simply inherit from the class (yes, this is designed to work with class-based views).
|
|
20
|
+
The [`HTMXView`](./views.py#HTMXView) class is the starting point for the server-side HTMX behavior. To use these features on a view, inherit from this class (yes, this is designed to work with class-based views).
|
|
27
21
|
|
|
28
22
|
```python
|
|
29
|
-
|
|
23
|
+
# app/views.py
|
|
24
|
+
from plain.htmx.views import HTMXView
|
|
30
25
|
|
|
31
|
-
from plain.htmx.views import HTMXViewMixin
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
class HomeView(HTMXViewMixin, TemplateView):
|
|
27
|
+
class HomeView(HTMXView):
|
|
35
28
|
template_name = "home.html"
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
In your `base.html` template (or wherever need the HTMX scripts),
|
|
39
|
-
you can use the `{% htmx_js %}` template tag:
|
|
31
|
+
In your `base.html` template (or wherever you need the HTMX scripts), you can use the `{% htmx_js %}` template tag:
|
|
40
32
|
|
|
41
33
|
```html
|
|
42
|
-
<!-- base.
|
|
34
|
+
<!-- base.html -->
|
|
43
35
|
{% load htmx %}
|
|
44
36
|
<!DOCTYPE html>
|
|
45
37
|
<html lang="en">
|
|
@@ -53,12 +45,9 @@ you can use the `{% htmx_js %}` template tag:
|
|
|
53
45
|
</body>
|
|
54
46
|
```
|
|
55
47
|
|
|
56
|
-
## Template
|
|
48
|
+
## Template fragments
|
|
57
49
|
|
|
58
|
-
An `{% htmxfragment %}` can
|
|
59
|
-
When you use a fragment, all `hx-get`, `hx-post`, etc. elements inside that fragment will automatically send a request to the current URL,
|
|
60
|
-
render _only_ the updated content for the fragment,
|
|
61
|
-
and swap out the fragment.
|
|
50
|
+
An `{% htmxfragment %}` can render a specific part of your template in HTMX responses. When you use a fragment, all `hx-get`, `hx-post`, etc. elements inside that fragment will automatically send a request to the current URL, render _only_ the updated content for the fragment, and swap out the fragment.
|
|
62
51
|
|
|
63
52
|
Here's an example:
|
|
64
53
|
|
|
@@ -87,8 +76,7 @@ Everything inside `{% htmxfragment %}` will automatically update when "Refresh"
|
|
|
87
76
|
|
|
88
77
|
### Lazy template fragments
|
|
89
78
|
|
|
90
|
-
If you want to render a fragment lazily,
|
|
91
|
-
you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
|
|
79
|
+
If you want to render a fragment lazily, you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
|
|
92
80
|
|
|
93
81
|
```html
|
|
94
82
|
{% htmxfragment "main" lazy=True %}
|
|
@@ -96,8 +84,7 @@ you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
|
|
|
96
84
|
{% endhtmxfragment %}
|
|
97
85
|
```
|
|
98
86
|
|
|
99
|
-
This pairs nicely with passing a callable function or method as a context variable,
|
|
100
|
-
which will only get invoked when the fragment actually gets rendered on the lazy load.
|
|
87
|
+
This pairs nicely with passing a callable function or method as a context variable, which will only get invoked when the fragment actually gets rendered on the lazy load.
|
|
101
88
|
|
|
102
89
|
```python
|
|
103
90
|
def fetch_items():
|
|
@@ -106,9 +93,9 @@ def fetch_items():
|
|
|
106
93
|
return ["foo", "bar", "baz"]
|
|
107
94
|
|
|
108
95
|
|
|
109
|
-
class HomeView(
|
|
110
|
-
def
|
|
111
|
-
context = super().
|
|
96
|
+
class HomeView(HTMXView):
|
|
97
|
+
def get_template_context(self):
|
|
98
|
+
context = super().get_template_context()
|
|
112
99
|
context["items"] = fetch_items # Missing () are on purpose!
|
|
113
100
|
return context
|
|
114
101
|
```
|
|
@@ -123,10 +110,9 @@ class HomeView(HTMXViewMixin, TemplateView):
|
|
|
123
110
|
{% endhtmxfragment %}
|
|
124
111
|
```
|
|
125
112
|
|
|
126
|
-
|
|
113
|
+
### How template fragments work
|
|
127
114
|
|
|
128
|
-
When you use the `{% htmxfragment %}` tag,
|
|
129
|
-
a standard `div` is output that looks like this:
|
|
115
|
+
When you use the `{% htmxfragment %}` tag, a standard `div` is output that looks like this:
|
|
130
116
|
|
|
131
117
|
```html
|
|
132
118
|
<div plain-hx-fragment="main" hx-swap="outerHTML" hx-target="this" hx-indicator="this">
|
|
@@ -134,39 +120,25 @@ a standard `div` is output that looks like this:
|
|
|
134
120
|
</div>
|
|
135
121
|
```
|
|
136
122
|
|
|
137
|
-
The `plain-hx-fragment` is a custom attribute
|
|
138
|
-
but the rest are standard HTMX attributes.
|
|
123
|
+
The `plain-hx-fragment` is a custom attribute, but the rest are standard HTMX attributes.
|
|
139
124
|
|
|
140
|
-
When Plain renders the response to an HTMX request,
|
|
141
|
-
it will get the `Plain-HX-Fragment` header,
|
|
142
|
-
find the fragment with that name in the template,
|
|
143
|
-
and render that for the response.
|
|
125
|
+
When Plain renders the response to an HTMX request, it will get the `Plain-HX-Fragment` header, find the fragment with that name in the template, and render that for the response.
|
|
144
126
|
|
|
145
127
|
Then the response content is automatically swapped in to replace the content of your `{% htmxfragment %}` tag.
|
|
146
128
|
|
|
147
|
-
Note that there is no URL specified on the `hx-get` attribute.
|
|
148
|
-
By default, HTMX will send the request to the current URL for the page.
|
|
149
|
-
When you're working with fragments, this is typically the behavior you want!
|
|
150
|
-
(You're on a page and want to selectively re-render a part of that page.)
|
|
129
|
+
Note that there is no URL specified on the `hx-get` attribute. By default, HTMX will send the request to the current URL for the page. When you're working with fragments, this is typically the behavior you want! (You're on a page and want to selectively re-render a part of that page.)
|
|
151
130
|
|
|
152
|
-
The `{% htmxfragment %}` tag is somewhat similar to a `{% block %}` tag --
|
|
153
|
-
the fragments on a page should be named and unique,
|
|
154
|
-
and you can't use it inside of loops.
|
|
155
|
-
For fragment-like behavior inside of a for-loop,
|
|
156
|
-
you'll most likely want to set up a dedicated URL that can handle a single instance of the looped items,
|
|
157
|
-
and maybe leverage [dedicated templates](#dedicated-templates).
|
|
131
|
+
The `{% htmxfragment %}` tag is somewhat similar to a `{% block %}` tag -- the fragments on a page should be named and unique, and you can't use it inside of loops. For fragment-like behavior inside of a for-loop, you'll most likely want to set up a dedicated URL that can handle a single instance of the looped items, and maybe leverage [dedicated templates](#dedicated-templates).
|
|
158
132
|
|
|
159
|
-
## View
|
|
133
|
+
## View actions
|
|
160
134
|
|
|
161
|
-
View actions let you define multiple "actions" on a class-based view.
|
|
162
|
-
This is an alternative to defining specific API endpoints or form views to handle basic button interactions.
|
|
135
|
+
View actions let you define multiple "actions" on a class-based view. This is an alternative to defining specific API endpoints or form views to handle basic button interactions.
|
|
163
136
|
|
|
164
|
-
With view actions you can design a single view that renders a single template,
|
|
165
|
-
and associate buttons in that template with class methods in the view.
|
|
137
|
+
With view actions you can design a single view that renders a single template, and associate buttons in that template with class methods in the view.
|
|
166
138
|
|
|
167
139
|
As an example, let's say we have a `PullRequest` model and we want users to be able to open, close, or merge it with a button.
|
|
168
140
|
|
|
169
|
-
In
|
|
141
|
+
In your template, use the `plain-hx-action` attribute to name the action:
|
|
170
142
|
|
|
171
143
|
```html
|
|
172
144
|
{% extends "base.html" %}
|
|
@@ -196,10 +168,14 @@ In our template, we would use the `plain-hx-action` attribute to name the action
|
|
|
196
168
|
{% endblock %}
|
|
197
169
|
```
|
|
198
170
|
|
|
199
|
-
Then in the view class,
|
|
171
|
+
Then in the view class, define methods for each HTTP method + `plain-hx-action`:
|
|
200
172
|
|
|
201
173
|
```python
|
|
202
|
-
|
|
174
|
+
from plain.htmx.views import HTMXView
|
|
175
|
+
from plain.views import DetailView
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class PullRequestDetailView(HTMXView, DetailView):
|
|
203
179
|
def get_queryset(self):
|
|
204
180
|
# The queryset will apply to all actions on the view, so "permission" logic can be shared
|
|
205
181
|
return super().get_queryset().filter(users=self.user)
|
|
@@ -213,44 +189,39 @@ class PullRequestDetailView(HTMXViewMixin, DetailView):
|
|
|
213
189
|
self.object.state = "closed"
|
|
214
190
|
self.object.save()
|
|
215
191
|
|
|
216
|
-
# Render the updated content the standard calls
|
|
217
|
-
# (which will selectively render
|
|
218
|
-
|
|
219
|
-
return self.render_to_response(context)
|
|
192
|
+
# Render the updated content with the standard calls
|
|
193
|
+
# (which will selectively render the fragment if applicable)
|
|
194
|
+
return self.render_template()
|
|
220
195
|
|
|
221
196
|
def htmx_post_close(self):
|
|
222
197
|
if self.object.state != "open":
|
|
223
|
-
raise ValueError("Only
|
|
198
|
+
raise ValueError("Only an open pull request can be closed")
|
|
224
199
|
|
|
225
200
|
self.object.state = "open"
|
|
226
201
|
self.object.save()
|
|
227
202
|
|
|
228
|
-
|
|
229
|
-
return self.render_to_response(context)
|
|
203
|
+
return self.render_template()
|
|
230
204
|
|
|
231
205
|
def htmx_post_merge(self):
|
|
232
206
|
if self.object.state != "open":
|
|
233
|
-
raise ValueError("Only
|
|
207
|
+
raise ValueError("Only an open pull request can be merged")
|
|
234
208
|
|
|
235
209
|
self.object.state = "merged"
|
|
236
210
|
self.object.save()
|
|
237
211
|
|
|
238
|
-
|
|
239
|
-
return self.render_to_response(context)
|
|
212
|
+
return self.render_template()
|
|
240
213
|
```
|
|
241
214
|
|
|
242
|
-
This can be a matter of preference,
|
|
243
|
-
but typically you may end up building out an entire form, API, or set of URLs to handle these behaviors.
|
|
244
|
-
If you application is only going to handle these actions via HTMX,
|
|
245
|
-
then a single View may be a simpler way to do it.
|
|
215
|
+
This can be a matter of preference, but typically you may end up building out an entire form, API, or set of URLs to handle these behaviors. If your application is only going to handle these actions via HTMX, then a single View may be a simpler way to do it.
|
|
246
216
|
|
|
247
|
-
|
|
248
|
-
this can basically all be done through standard request and response headers:
|
|
217
|
+
You can also handle HTMX requests without a specific action by just implementing the HTTP method:
|
|
249
218
|
|
|
250
219
|
```python
|
|
251
|
-
|
|
220
|
+
from plain.http import HttpResponse
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class PullRequestDetailView(HTMXView, DetailView):
|
|
252
224
|
def get_queryset(self):
|
|
253
|
-
# The queryset will apply to all actions on the view, so "permission" logic can be shared
|
|
254
225
|
return super().get_queryset().filter(users=self.user)
|
|
255
226
|
|
|
256
227
|
# You can also leave off the "plain-hx-action" attribute and just handle the HTTP method
|
|
@@ -263,18 +234,13 @@ class PullRequestDetailView(HTMXViewMixin, DetailView):
|
|
|
263
234
|
return response
|
|
264
235
|
```
|
|
265
236
|
|
|
266
|
-
## Dedicated
|
|
237
|
+
## Dedicated templates
|
|
267
238
|
|
|
268
|
-
A small additional
|
|
269
|
-
More than anything, this is just a nice way to formalize a naming scheme for template "partials" dedicated to HTMX.
|
|
239
|
+
A small additional feature is that `plain.htmx` will automatically find templates named `{template_name}_htmx.html` for HTMX requests. More than anything, this is just a nice way to formalize a naming scheme for template "partials" dedicated to HTMX.
|
|
270
240
|
|
|
271
|
-
Because template fragments don't work inside of loops,
|
|
272
|
-
for example,
|
|
273
|
-
you'll often need to define dedicated URLs to handle the HTMX behaviors for individual items in a loop.
|
|
274
|
-
You can sometimes think of these as "pages within a page".
|
|
241
|
+
Because template fragments don't work inside of loops, for example, you'll often need to define dedicated URLs to handle the HTMX behaviors for individual items in a loop. You can sometimes think of these as "pages within a page".
|
|
275
242
|
|
|
276
|
-
So if you have a template that renders a collection of items,
|
|
277
|
-
you can do the initial render using a Django `{% include %}`:
|
|
243
|
+
So if you have a template that renders a collection of items, you can do the initial render using a `{% include %}`:
|
|
278
244
|
|
|
279
245
|
```html
|
|
280
246
|
<!-- pullrequests/pullrequest_list.html -->
|
|
@@ -326,21 +292,20 @@ urlpatterns = [
|
|
|
326
292
|
]
|
|
327
293
|
|
|
328
294
|
# views.py
|
|
329
|
-
class PullRequestDetailView(
|
|
295
|
+
class PullRequestDetailView(HTMXView, DetailView):
|
|
330
296
|
def htmx_post_update(self):
|
|
331
297
|
self.object.update()
|
|
332
298
|
|
|
333
|
-
|
|
334
|
-
return self.render_to_response(context)
|
|
299
|
+
return self.render_template()
|
|
335
300
|
```
|
|
336
301
|
|
|
337
|
-
##
|
|
302
|
+
## FAQs
|
|
338
303
|
|
|
339
|
-
|
|
340
|
-
This tells HTMX to add the `htmx-request` class to the fragment element when it is loading.
|
|
304
|
+
#### How do I add a Tailwind CSS variant for loading states?
|
|
341
305
|
|
|
342
|
-
|
|
343
|
-
|
|
306
|
+
The standard behavior for `{% htmxfragment %}` is to set `hx-indicator="this"` on the rendered element. This tells HTMX to add the `htmx-request` class to the fragment element when it is loading.
|
|
307
|
+
|
|
308
|
+
Here's a simple variant you can add to your `tailwind.config.js` to easily style the loading state:
|
|
344
309
|
|
|
345
310
|
```js
|
|
346
311
|
const plugin = require('tailwindcss/plugin')
|
|
@@ -368,14 +333,13 @@ You can then prefix any class with `htmx-request:` to decide what it looks like
|
|
|
368
333
|
</form>
|
|
369
334
|
```
|
|
370
335
|
|
|
371
|
-
|
|
336
|
+
#### How are CSRF tokens handled?
|
|
372
337
|
|
|
373
|
-
|
|
374
|
-
You don't have to put `hx-headers` on the `<body>` tag, for example.
|
|
338
|
+
CSRF tokens are configured automatically with the HTMX JS API. You don't have to put `hx-headers` on the `<body>` tag.
|
|
375
339
|
|
|
376
|
-
|
|
340
|
+
#### How do I show error states?
|
|
377
341
|
|
|
378
|
-
This
|
|
342
|
+
This package includes an HTMX extension for adding error classes for failed requests:
|
|
379
343
|
|
|
380
344
|
- `htmx-error-response` for `htmx:responseError`
|
|
381
345
|
- `htmx-error-response-{{ status_code }}` for `htmx:responseError`
|
|
@@ -383,7 +347,7 @@ This app also includes an HTMX extension for adding error classes for failed req
|
|
|
383
347
|
|
|
384
348
|
To enable them, use `hx-ext="plain-errors"`.
|
|
385
349
|
|
|
386
|
-
You can add the ones you want as Tailwind variants and use them to show error messages
|
|
350
|
+
You can add the ones you want as Tailwind variants and use them to show error messages:
|
|
387
351
|
|
|
388
352
|
```js
|
|
389
353
|
const plugin = require('tailwindcss/plugin')
|
|
@@ -396,9 +360,11 @@ module.exports = {
|
|
|
396
360
|
}
|
|
397
361
|
```
|
|
398
362
|
|
|
399
|
-
|
|
363
|
+
#### How do I configure HTMX for CSP?
|
|
400
364
|
|
|
401
|
-
|
|
365
|
+
If you're using Content Security Policy, you can disable the indicator styles that HTMX adds inline:
|
|
366
|
+
|
|
367
|
+
```html
|
|
402
368
|
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
|
|
403
369
|
```
|
|
404
370
|
|
|
@@ -410,11 +376,55 @@ Install the `plain.htmx` package from [PyPI](https://pypi.org/project/plain.htmx
|
|
|
410
376
|
uv add plain.htmx
|
|
411
377
|
```
|
|
412
378
|
|
|
413
|
-
|
|
379
|
+
Add `plain.htmx` to your installed packages:
|
|
414
380
|
|
|
415
381
|
```python
|
|
382
|
+
# app/settings.py
|
|
416
383
|
INSTALLED_PACKAGES = [
|
|
417
384
|
# ...
|
|
418
385
|
"plain.htmx",
|
|
419
386
|
]
|
|
420
387
|
```
|
|
388
|
+
|
|
389
|
+
Add the HTMX JavaScript to your base template:
|
|
390
|
+
|
|
391
|
+
```html
|
|
392
|
+
<!-- base.html -->
|
|
393
|
+
{% load htmx %}
|
|
394
|
+
<!DOCTYPE html>
|
|
395
|
+
<html lang="en">
|
|
396
|
+
<head>
|
|
397
|
+
<meta charset="UTF-8">
|
|
398
|
+
<title>My Site</title>
|
|
399
|
+
{% htmx_js %}
|
|
400
|
+
</head>
|
|
401
|
+
<body>
|
|
402
|
+
{% block content %}{% endblock %}
|
|
403
|
+
</body>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Create a view that inherits from `HTMXView`:
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
# app/views.py
|
|
410
|
+
from plain.htmx.views import HTMXView
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class HomeView(HTMXView):
|
|
414
|
+
template_name = "home.html"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Create a template with an HTMX fragment:
|
|
418
|
+
|
|
419
|
+
```html
|
|
420
|
+
<!-- home.html -->
|
|
421
|
+
{% extends "base.html" %}
|
|
422
|
+
{% load htmx %}
|
|
423
|
+
|
|
424
|
+
{% block content %}
|
|
425
|
+
{% htmxfragment "content" %}
|
|
426
|
+
<p>The time is {% now "jS F Y H:i" %}</p>
|
|
427
|
+
<button hx-get>Refresh</button>
|
|
428
|
+
{% endhtmxfragment %}
|
|
429
|
+
{% endblock %}
|
|
430
|
+
```
|
|
@@ -26,7 +26,7 @@ class HTMXJSExtension(InclusionTagExtension):
|
|
|
26
26
|
return {
|
|
27
27
|
"DEBUG": settings.DEBUG,
|
|
28
28
|
"extensions": kwargs.get("extensions", []),
|
|
29
|
-
"csp_nonce": request.csp_nonce if request else None,
|
|
29
|
+
"csp_nonce": request.csp_nonce if request else None,
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
|
|
@@ -167,5 +167,5 @@ def find_template_fragment(
|
|
|
167
167
|
)
|
|
168
168
|
|
|
169
169
|
# Create a new template from the node
|
|
170
|
-
template_node = nodes.Template(callblock_node.body)
|
|
171
|
-
return template.environment.from_string(template_node)
|
|
170
|
+
template_node = nodes.Template(callblock_node.body)
|
|
171
|
+
return template.environment.from_string(template_node)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-ext.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-ext.min.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-htmx.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.amd.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.cjs.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.esm.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.min.js
RENAMED
|
File without changes
|
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/ajax-header.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/alpine-morph.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/class-tools.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/disable-element.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/event-header.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/head-support.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/include-vals.js
RENAMED
|
File without changes
|
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/loading-states.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/method-override.js
RENAMED
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/morphdom-swap.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/path-params.js
RENAMED
|
File without changes
|
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/rails-method.js
RENAMED
|
File without changes
|
|
File without changes
|
{plain_htmx-0.15.0 → plain_htmx-0.16.0}/plain/htmx/assets/htmx/vendor/src/ext/response-targets.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|