plain.htmx 0.15.1__tar.gz → 0.16.1__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.
Files changed (57) hide show
  1. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/PKG-INFO +115 -105
  2. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/CHANGELOG.md +20 -0
  3. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/README.md +114 -104
  4. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/views.py +2 -0
  5. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/pyproject.toml +1 -1
  6. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/.gitignore +0 -0
  7. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/LICENSE +0 -0
  8. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/README.md +0 -0
  9. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/deps.yml +0 -0
  10. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/package-lock.json +0 -0
  11. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/package.json +0 -0
  12. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/__init__.py +0 -0
  13. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/plainhtmx.js +0 -0
  14. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-ext.js +0 -0
  15. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-ext.min.js +0 -0
  16. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph-htmx.js +0 -0
  17. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.amd.js +0 -0
  18. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.cjs.js +0 -0
  19. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.esm.js +0 -0
  20. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.js +0 -0
  21. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/idiomorph/idiomorph.min.js +0 -0
  22. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/README.md +0 -0
  23. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/ajax-header.js +0 -0
  24. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/alpine-morph.js +0 -0
  25. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/class-tools.js +0 -0
  26. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/client-side-templates.js +0 -0
  27. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/debug.js +0 -0
  28. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/disable-element.js +0 -0
  29. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/event-header.js +0 -0
  30. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/head-support.js +0 -0
  31. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/include-vals.js +0 -0
  32. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/json-enc.js +0 -0
  33. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/loading-states.js +0 -0
  34. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/method-override.js +0 -0
  35. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/morphdom-swap.js +0 -0
  36. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/multi-swap.js +0 -0
  37. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/path-deps.js +0 -0
  38. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/path-params.js +0 -0
  39. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/preload.js +0 -0
  40. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/rails-method.js +0 -0
  41. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/remove-me.js +0 -0
  42. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/response-targets.js +0 -0
  43. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/restored.js +0 -0
  44. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/sse.js +0 -0
  45. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/ext/ws.js +0 -0
  46. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/htmx.amd.js +0 -0
  47. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/htmx.cjs.js +0 -0
  48. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/htmx.esm.d.ts +0 -0
  49. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/htmx.esm.js +0 -0
  50. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/htmx.js +0 -0
  51. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/htmx.min.js +0 -0
  52. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/assets/htmx/vendor/src/htmx.min.js.gz +0 -0
  53. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/templates/htmx/js.html +0 -0
  54. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/plain/htmx/templates.py +0 -0
  55. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/tests/app/settings.py +0 -0
  56. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/tests/app/urls.py +0 -0
  57. {plain_htmx-0.15.1 → plain_htmx-0.16.1}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.htmx
3
- Version: 0.15.1
3
+ Version: 0.16.1
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 Fragments](#template-fragments)
17
+ - [Template fragments](#template-fragments)
18
18
  - [Lazy template fragments](#lazy-template-fragments)
19
- - [How does it work?](#how-does-it-work)
20
- - [View Actions](#view-actions)
21
- - [Dedicated Templates](#dedicated-templates)
22
- - [Tailwind CSS variant](#tailwind-css-variant)
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
- The `plain-htmx` package adds a couple of unique features for working with HTMX.
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 combination of these features lets you build HTMX-powered views that focus on server-side rendering and avoid overly complicated URL structures or REST APIs that you may not otherwise need.
29
+ The two main features are [template fragments](#template-fragments) and [view actions](#view-actions).
34
30
 
35
- The [`HTMXViewMixin`](./views.py#HTMXViewMixin) is the starting point for the server-side HTMX behavior.
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
- from plain.views import TemplateView
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.template.html -->
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 Fragments
59
+ ## Template fragments
68
60
 
69
- An `{% htmxfragment %}` can be used to render a specific part of your template in HTMX responses.
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(HTMXViewMixin, TemplateView):
121
- def get_context(self, **kwargs):
122
- context = super().get_context(**kwargs)
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
- #### How does it work?
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 that we've added ("F" is for "Forge"),
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 Actions
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 our template, we would use the `plain-hx-action` attribute to name the action:
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, we can define methods for each HTTP method + `plain-hx-action`:
182
+ Then in the view class, define methods for each HTTP method + `plain-hx-action`:
211
183
 
212
184
  ```python
213
- class PullRequestDetailView(HTMXViewMixin, DetailView):
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 our fragment if applicable)
229
- context = self.get_context(object=self.object)
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 a open pull request can be closed")
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
- context = self.get_context(object=self.object)
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 a open pull request can be merged")
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
- context = self.get_context(object=self.object)
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
- Note that currently we don't have many helper-functions for parsing or returning HTMX responses --
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
- class PullRequestDetailView(HTMXViewMixin, DetailView):
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 Templates
248
+ ## Dedicated templates
278
249
 
279
- A small additional features of `plain-htmx` is that it will automatically find templates named `{template_name}_htmx.html` for HTMX requests.
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(HTMXViewMixin, DetailView):
306
+ class PullRequestDetailView(HTMXView, DetailView):
341
307
  def htmx_post_update(self):
342
308
  self.object.update()
343
309
 
344
- context = self.get_context(object=self.object)
345
- return self.render_to_response(context)
310
+ return self.render_template()
346
311
  ```
347
312
 
348
- ## Tailwind CSS variant
313
+ ## FAQs
349
314
 
350
- The standard behavior for `{% htmxfragment %}` is to set `hx-indicator="this"` on the rendered element.
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
- Since Plain emphasizes using Tailwind CSS,
354
- here's a simple variant you can add to your `tailwind.config.js` to easily style the loading state:
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
- ## CSRF tokens
347
+ #### How are CSRF tokens handled?
383
348
 
384
- We configure CSRF tokens for you with the HTMX JS API.
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
- ## Error classes
351
+ #### How do I show error states?
388
352
 
389
- This app also includes an HTMX extension for adding error classes for failed requests.
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
- ## CSP
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
- Configure your Plain application:
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.1](https://github.com/dropseed/plain/releases/plain-htmx@0.16.1) (2026-02-04)
4
+
5
+ ### What's changed
6
+
7
+ - Added `__all__` export to `views` module for explicit public API boundaries ([f26a63a5c941](https://github.com/dropseed/plain/commit/f26a63a5c941))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required.
12
+
13
+ ## [0.16.0](https://github.com/dropseed/plain/releases/plain-htmx@0.16.0) (2026-01-13)
14
+
15
+ ### What's changed
16
+
17
+ - Improved README documentation with better structure, examples, and FAQs section ([da37a78](https://github.com/dropseed/plain/commit/da37a78))
18
+
19
+ ### Upgrade instructions
20
+
21
+ - No changes required
22
+
3
23
  ## [0.15.1](https://github.com/dropseed/plain/releases/plain-htmx@0.15.1) (2025-12-22)
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 Fragments](#template-fragments)
6
+ - [Template fragments](#template-fragments)
7
7
  - [Lazy template fragments](#lazy-template-fragments)
8
- - [How does it work?](#how-does-it-work)
9
- - [View Actions](#view-actions)
10
- - [Dedicated Templates](#dedicated-templates)
11
- - [Tailwind CSS variant](#tailwind-css-variant)
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
- The `plain-htmx` package adds a couple of unique features for working with HTMX.
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 combination of these features lets you build HTMX-powered views that focus on server-side rendering and avoid overly complicated URL structures or REST APIs that you may not otherwise need.
18
+ The two main features are [template fragments](#template-fragments) and [view actions](#view-actions).
23
19
 
24
- The [`HTMXViewMixin`](./views.py#HTMXViewMixin) is the starting point for the server-side HTMX behavior.
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
- from plain.views import TemplateView
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.template.html -->
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 Fragments
48
+ ## Template fragments
57
49
 
58
- An `{% htmxfragment %}` can be used to render a specific part of your template in HTMX responses.
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(HTMXViewMixin, TemplateView):
110
- def get_context(self, **kwargs):
111
- context = super().get_context(**kwargs)
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
- #### How does it work?
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 that we've added ("F" is for "Forge"),
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 Actions
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 our template, we would use the `plain-hx-action` attribute to name the action:
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, we can define methods for each HTTP method + `plain-hx-action`:
171
+ Then in the view class, define methods for each HTTP method + `plain-hx-action`:
200
172
 
201
173
  ```python
202
- class PullRequestDetailView(HTMXViewMixin, DetailView):
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 our fragment if applicable)
218
- context = self.get_context(object=self.object)
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 a open pull request can be closed")
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
- context = self.get_context(object=self.object)
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 a open pull request can be merged")
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
- context = self.get_context(object=self.object)
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
- Note that currently we don't have many helper-functions for parsing or returning HTMX responses --
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
- class PullRequestDetailView(HTMXViewMixin, DetailView):
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 Templates
237
+ ## Dedicated templates
267
238
 
268
- A small additional features of `plain-htmx` is that it will automatically find templates named `{template_name}_htmx.html` for HTMX requests.
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(HTMXViewMixin, DetailView):
295
+ class PullRequestDetailView(HTMXView, DetailView):
330
296
  def htmx_post_update(self):
331
297
  self.object.update()
332
298
 
333
- context = self.get_context(object=self.object)
334
- return self.render_to_response(context)
299
+ return self.render_template()
335
300
  ```
336
301
 
337
- ## Tailwind CSS variant
302
+ ## FAQs
338
303
 
339
- The standard behavior for `{% htmxfragment %}` is to set `hx-indicator="this"` on the rendered element.
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
- Since Plain emphasizes using Tailwind CSS,
343
- here's a simple variant you can add to your `tailwind.config.js` to easily style the loading state:
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
- ## CSRF tokens
336
+ #### How are CSRF tokens handled?
372
337
 
373
- We configure CSRF tokens for you with the HTMX JS API.
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
- ## Error classes
340
+ #### How do I show error states?
377
341
 
378
- This app also includes an HTMX extension for adding error classes for failed requests.
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
- ## CSP
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
- Configure your Plain application:
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
+ ```
@@ -9,6 +9,8 @@ from plain.views import TemplateView
9
9
 
10
10
  from .templates import render_template_fragment
11
11
 
12
+ __all__ = ["HTMXView"]
13
+
12
14
 
13
15
  class HTMXView(TemplateView):
14
16
  """View with HTMX-specific functionality."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.htmx"
3
- version = "0.15.1"
3
+ version = "0.16.1"
4
4
  description = "Integrate HTMX with templates and views."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  license = "BSD-3-Clause"
File without changes
File without changes
File without changes
File without changes
File without changes