1# HTMX
2
3Integrate HTMX with templates and views.
4
5The `plain-htmx` package adds a couple of unique features for working with HTMX.
6One is [template fragments](#template-fragments) and the other is [view actions](#view-actions).
7
8The 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.
9
10The `HTMXViewMixin` is the starting point for the server-side HTMX behavior.
11To use these feaures on a view,
12simply inherit from the class (yes, this is designed to work with class-based views).
13
14```python
15from plain.views import TemplateView
16
17from plain.htmx.views import HTMXViewMixin
18
19
20class HomeView(HTMXViewMixin, TemplateView):
21 template_name = "home.html"
22```
23
24In your `base.html` template (or wherever need the HTMX scripts),
25you can use the `{% htmx_js %}` template tag:
26
27```html
28<!-- base.template.html -->
29{% load htmx %}
30<!DOCTYPE html>
31<html lang="en">
32<head>
33 <meta charset="UTF-8">
34 <title>My Site</title>
35 {% htmx_js %}
36</head>
37<body>
38 {% block content %}{% endblock %}
39</body>
40```
41
42## Installation
43
44```python
45INSTALLED_PACKAGES = [
46 # ...
47 "plain.htmx",
48]
49```
50
51## Template Fragments
52
53An `{% htmxfragment %}` can be used to render a specific part of your template in HTMX responses.
54When you use a fragment, all `hx-get`, `hx-post`, etc. elements inside that fragment will automatically send a request to the current URL,
55render _only_ the updated content for the fragment,
56and swap out the fragment.
57
58Here's an example:
59
60```html
61<!-- home.html -->
62{% extends "base.html" %}
63
64{% load htmx %}
65
66{% block content %}
67<header>
68 <h1>Page title</h1>
69</header>
70
71<main>
72 {% htmxfragment main %}
73 <p>The time is {% now "jS F Y H:i" %}</p>
74
75 <button hx-get>Refresh</button>
76 {% endhtmxfragment %}
77</main>
78{% endblock %}
79```
80
81Everything inside `{% htmxfragment %}` will automatically update when "Refresh" is clicked.
82
83### Lazy template fragments
84
85If you want to render a fragment lazily,
86you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
87
88```html
89{% htmxfragment main lazy=True %}
90<!-- This content will be fetched with hx-get -->
91{% endhtmxfragment %}
92```
93
94This pairs nicely with passing a callable function or method as a context variable,
95which will only get invoked when the fragment actually gets rendered on the lazy load.
96
97```python
98def fetch_items():
99 import time
100 time.sleep(2)
101 return ["foo", "bar", "baz"]
102
103
104class HomeView(HTMXViewMixin, TemplateView):
105 def get_context(self, **kwargs):
106 context = super().get_context(**kwargs)
107 context["items"] = fetch_items # Missing () are on purpose!
108 return context
109```
110
111```html
112{% htmxfragment main lazy=True %}
113<ul>
114 {% for item in items %}
115 <li>{{ item }}</li>
116 {% endfor %}
117</ul>
118{% endhtmxfragment %}
119```
120
121### How does it work?
122
123When you use the `{% htmxfragment %}` tag,
124a standard `div` is output that looks like this:
125
126```html
127<div plain-hx-fragment="main" hx-swap="outerHTML" hx-target="this" hx-indicator="this">
128 {{ fragment_content }}
129</div>
130```
131
132The `plain-hx-fragment` is a custom attribute that we've added ("F" is for "Forge"),
133but the rest are standard HTMX attributes.
134
135When Plain renders the response to an HTMX request,
136it will get the `Plain-HX-Fragment` header,
137find the fragment with that name in the template,
138and render that for the response.
139
140Then the response content is automatically swapped in to replace the content of your `{% htmxfragment %}` tag.
141
142Note that there is no URL specified on the `hx-get` attribute.
143By default, HTMX will send the request to the current URL for the page.
144When you're working with fragments, this is typically the behavior you want!
145(You're on a page and want to selectively re-render a part of that page.)
146
147The `{% htmxfragment %}` tag is somewhat similar to a `{% block %}` tag --
148the fragments on a page should be named and unique,
149and you can't use it inside of loops.
150For fragment-like behavior inside of a for-loop,
151you'll most likely want to set up a dedicated URL that can handle a single instance of the looped items,
152and maybe leverage [dedicated templates](#dedicated-templates).
153
154## View Actions
155
156View actions let you define multiple "actions" on a class-based view.
157This is an alternative to defining specific API endpoints or form views to handle basic button interactions.
158
159With view actions you can design a single view that renders a single template,
160and associate buttons in that template with class methods in the view.
161
162As 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.
163
164In our template, we would use the `plain-hx-action` attribute to name the action:
165
166```html
167{% extends "base.html" %}
168
169{% load htmx %}
170
171{% block content %}
172<header>
173 <h1>{{ pullrequest }}</h1>
174</header>
175
176<main>
177 {% htmxfragment pullrequest %}
178 <p>State: {{ pullrequest.state }}</p>
179
180 {% if pullrequest.state == "open" %}
181 <!-- If it's open, they can close or merge it -->
182 <button hx-post plain-hx-action="close">Close</button>
183 <button hx-post plain-hx-action="merge">Merge</button>
184 {% else if pullrequest.state == "closed" %}
185 <!-- If it's closed, it can be re-opened -->
186 <button hx-post plain-hx-action="open">Open</button>
187 {% endif %}
188
189 {% endhtmxfragment %}
190</main>
191{% endblock %}
192```
193
194Then in the view class, we can define methods for each HTTP method + `plain-hx-action`:
195
196```python
197class PullRequestDetailView(HTMXViewMixin, DetailView):
198 def get_queryset(self):
199 # The queryset will apply to all actions on the view, so "permission" logic can be shared
200 return super().get_queryset().filter(users=self.request.user)
201
202 # Action handling methods follow this format:
203 # htmx_{method}_{action}
204 def htmx_post_open(self):
205 self.object = self.get_object()
206
207 if self.object.state != "closed":
208 raise ValueError("Only a closed pull request can be opened")
209
210 self.object.state = "closed"
211 self.object.save()
212
213 # Render the updated content the standard calls
214 # (which will selectively render our fragment if applicable)
215 context = self.get_context(object=self.object)
216 return self.render_to_response(context)
217
218 def htmx_post_close(self):
219 self.object = self.get_object()
220
221 if self.object.state != "open":
222 raise ValueError("Only a open pull request can be closed")
223
224 self.object.state = "open"
225 self.object.save()
226
227 context = self.get_context(object=self.object)
228 return self.render_to_response(context)
229
230 def htmx_post_merge(self):
231 self.object = self.get_object()
232
233 if self.object.state != "open":
234 raise ValueError("Only a open pull request can be merged")
235
236 self.object.state = "merged"
237 self.object.save()
238
239 context = self.get_context(object=self.object)
240 return self.render_to_response(context)
241```
242
243This can be a matter of preference,
244but typically you may end up building out an entire form, API, or set of URLs to handle these behaviors.
245If you application is only going to handle these actions via HTMX,
246then a single View may be a simpler way to do it.
247
248Note that currently we don't have many helper-functions for parsing or returning HTMX responses --
249this can basically all be done through standard request and response headers:
250
251```python
252class PullRequestDetailView(HTMXViewMixin, DetailView):
253 def get_queryset(self):
254 # The queryset will apply to all actions on the view, so "permission" logic can be shared
255 return super().get_queryset().filter(users=self.request.user)
256
257 # You can also leave off the "plain-hx-action" attribute and just handle the HTTP method
258 def htmx_delete(self):
259 self.object = self.get_object()
260
261 self.object.delete()
262
263 # Tell HTMX to do a client-side redirect when it receives the response
264 response = HttpResponse(status_code=204)
265 response.headers["HX-Redirect"] = "/"
266 return response
267```
268
269## Dedicated Templates
270
271A small additional features of `plain-htmx` is that it will automatically find templates named `{template_name}_htmx.html` for HTMX requests.
272More than anything, this is just a nice way to formalize a naming scheme for template "partials" dedicated to HTMX.
273
274Because template fragments don't work inside of loops,
275for example,
276you'll often need to define dedicated URLs to handle the HTMX behaviors for individual items in a loop.
277You can sometimes think of these as "pages within a page".
278
279So if you have a template that renders a collection of items,
280you can do the initial render using a Django `{% include %}`:
281
282```html
283<!-- pullrequests/pullrequest_list.html -->
284{% extends "base.html" %}
285
286{% block content %}
287
288{% for pullrequest in pullrequests %}
289<div>
290 {% include "pullrequests/pullrequest_detail_htmx.html" %}
291</div>
292{% endfor %}
293
294{% endblock %}
295```
296
297And then subsequent HTMX requests/actions on individual items can be handled by a separate URL/View:
298
299```html
300<!-- pullrequests/pullrequest_detail_htmx.html -->
301<div hx-url="{% url 'pullrequests:detail' pullrequest.uuid %}" hx-swap="outerHTML" hx-target="this">
302 <!-- Send all HTMX requests to a URL for single pull requests (works inside of a loop, or on a single detail page) -->
303 <h2>{{ pullrequest.title }}</h2>
304 <button hx-get>Refresh</button>
305 <button hx-post plain-hx-action="update">Update</button>
306</div>
307```
308
309_If_ you need a URL to render an individual item, you can simply include the same template fragment in most cases:
310
311```html
312<!-- pullrequests/pullrequest_detail.html -->
313{% extends "base.html" %}
314
315{% block content %}
316
317{% include "pullrequests/pullrequest_detail_htmx.html" %}
318
319{% endblock %}
320```
321
322```python
323# urls.py and views.py
324# urls.py
325default_namespace = "pullrequests"
326
327urlpatterns = [
328 path("<uuid:uuid>/", views.PullRequestDetailView, name="detail"),
329]
330
331# views.py
332class PullRequestDetailView(HTMXViewMixin, DetailView):
333 def htmx_post_update(self):
334 self.object = self.get_object()
335
336 self.object.update()
337
338 context = self.get_context(object=self.object)
339 return self.render_to_response(context)
340```
341
342## Tailwind CSS variant
343
344The standard behavior for `{% htmxfragment %}` is to set `hx-indicator="this"` on the rendered element.
345This tells HTMX to add the `htmx-request` class to the fragment element when it is loading.
346
347Since Plain emphasizes using Tailwind CSS,
348here's a simple variant you can add to your `tailwind.config.js` to easily style the loading state:
349
350```js
351const plugin = require('tailwindcss/plugin')
352
353module.exports = {
354 plugins: [
355 // Add variants for htmx-request class for loading states
356 plugin(({addVariant}) => addVariant('htmx-request', ['&.htmx-request', '.htmx-request &']))
357 ],
358}
359```
360
361You can then prefix any class with `htmx-request:` to decide what it looks like while HTMX requests are being sent:
362
363```html
364<!-- The "htmx-request" class will be added to the <form> by default -->
365<form hx-post="{{ url }}">
366 <!-- Showing an element -->
367 <div class="hidden htmx-request:block">
368 Loading
369 </div>
370
371 <!-- Changing a button's class -->
372 <button class="text-white bg-black htmx-request:opacity-50 htmx-request:cursor-wait" type="submit">Submit</button>
373</form>
374```
375
376## CSRF tokens
377
378We configure CSRF tokens for you with the HTMX JS API.
379You don't have to put `hx-headers` on the `<body>` tag, for example.
380
381## Error classes
382
383This app also includes an HTMX extension for adding error classes for failed requests.
384
385- `htmx-error-response` for `htmx:responseError`
386- `htmx-error-response-{{ status_code }}` for `htmx:responseError`
387- `htmx-error-send` for `htmx:sendError`
388
389To enable them, use `hx-ext="error-classes"`.
390
391You can add the ones you want as Tailwind variants and use them to show error messages.
392
393```js
394const plugin = require('tailwindcss/plugin')
395
396module.exports = {
397 plugins: [
398 // Add variants for htmx-request class for loading states
399 plugin(({addVariant}) => addVariant('htmx-error-response-429', ['&.htmx-error-response-429', '.htmx-error-response-429 &']))
400 ],
401}
402```
403
404## CSP
405
406```
407<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
408```